Merge pe-BUT-v2 de Cléo

This commit is contained in:
Emmanuel Viennet 2024-01-25 21:06:59 +01:00
commit 3a3d47ebe4
15 changed files with 2327 additions and 3400 deletions

68
app/pe/pe_affichage.py Normal file
View File

@ -0,0 +1,68 @@
from app.models import Formation, FormSemestre
from app.scodoc import codes_cursus
def nom_semestre_etape(semestre: FormSemestre, avec_fid=False) -> str:
"""Nom d'un semestre à afficher dans le descriptif des étapes de la scolarité
d'un étudiant.
Par ex: Pour un S2, affiche ``"Semestre 2 FI S014-2015 (129)"`` avec :
* 2 le numéro du semestre,
* FI la modalité,
* 2014-2015 les dates
Args:
semestre: Un ``FormSemestre``
avec_fid: Ajoute le n° du semestre à la description
Returns:
La chaine de caractères décrivant succintement le semestre
"""
formation: Formation = semestre.formation
parcours = codes_cursus.get_cursus_from_code(formation.type_parcours)
description = [
parcours.SESSION_NAME.capitalize(),
str(semestre.semestre_id),
semestre.modalite, # eg FI ou FC
f"{semestre.date_debut.year}-{semestre.date_fin.year}",
]
if avec_fid:
description.append(f"({semestre.forsemestre_id})")
return " ".join(description)
def etapes_du_cursus(semestres: dict[int, FormSemestre], nbre_etapes_max: int) -> list[str]:
"""Partant d'un dictionnaire de semestres (qui retrace
la scolarité d'un étudiant), liste les noms des
semestres (en version abbrégée)
qu'un étudiant a suivi au cours de sa scolarité à l'IUT.
Les noms des semestres sont renvoyés dans un dictionnaire
``{"etape i": nom_semestre_a_etape_i}``
avec i variant jusqu'à nbre_semestres_max. (S'il n'y a pas de semestre à l'étape i,
le nom affiché est vide.
La fonction suppose la liste des semestres triées par ordre
décroissant de date.
Args:
semestres: une liste de ``FormSemestre``
nbre_etapes_max: le nombre d'étapes max prise en compte
Returns:
Une liste de nom de semestre (dans le même ordre que les ``semestres``)
See also:
app.pe.pe_affichage.nom_semestre_etape
"""
assert len(semestres) <= nbre_etapes_max
noms = [nom_semestre_etape(sem, avec_fid=False) for (fid, sem) in semestres.items()]
noms = noms[::-1] # trie par ordre croissant
dico = {f"Etape {i+1}": "" for i in range(nbre_etapes_max)}
for (i, nom) in enumerate(noms): # Charge les noms de semestres
dico[f"Etape {i+1}"] = nom
return dico

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

384
app/pe/pe_comp.py Normal file
View File

@ -0,0 +1,384 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
##############################################################################
# Module "Avis de poursuite d'étude"
# conçu et développé par Cléo Baras (IUT de Grenoble)
##############################################################################
"""
Created on Thu Sep 8 09:36:33 2016
@author: barasc
"""
import os
import datetime
import re
import unicodedata
from flask import g
import app.scodoc.sco_utils as scu
from app import log
from app.models import FormSemestre
from app.scodoc import sco_formsemestre
from app.scodoc.sco_logos import find_logo
PE_DEBUG = 0
if not PE_DEBUG:
# log to notes.log
def pe_print(*a, **kw):
# kw is ignored. log always add a newline
log(" ".join(a))
else:
pe_print = print # print function
# Generated LaTeX files are encoded as:
PE_LATEX_ENCODING = "utf-8"
# /opt/scodoc/tools/doc_poursuites_etudes
REP_DEFAULT_AVIS = os.path.join(scu.SCO_TOOLS_DIR, "doc_poursuites_etudes/")
REP_LOCAL_AVIS = os.path.join(scu.SCODOC_CFG_DIR, "doc_poursuites_etudes/")
PE_DEFAULT_AVIS_LATEX_TMPL = REP_DEFAULT_AVIS + "distrib/modeles/un_avis.tex"
PE_LOCAL_AVIS_LATEX_TMPL = REP_LOCAL_AVIS + "local/modeles/un_avis.tex"
PE_DEFAULT_FOOTER_TMPL = REP_DEFAULT_AVIS + "distrib/modeles/un_footer.tex"
PE_LOCAL_FOOTER_TMPL = REP_LOCAL_AVIS + "local/modeles/un_footer.tex"
# ----------------------------------------------------------------------------------------
"""
Descriptif d'un parcours classique BUT
TODO:: A améliorer si BUT en moins de 6 semestres
"""
PARCOURS = {
"S1": {
"aggregat": ["S1"],
"ordre": 1,
"affichage_court": "S1",
"affichage_long": "Semestre 1",
},
"S2": {
"aggregat": ["S2"],
"ordre": 2,
"affichage_court": "S2",
"affichage_long": "Semestre 2",
},
"1A": {
"aggregat": ["S1", "S2"],
"ordre": 3,
"affichage_court": "1A",
"affichage_long": "1ère année",
},
"S3": {
"aggregat": ["S3"],
"ordre": 4,
"affichage_court": "S3",
"affichage_long": "Semestre 3",
},
"S4": {
"aggregat": ["S4"],
"ordre": 5,
"affichage_court": "S4",
"affichage_long": "Semestre 4",
},
"2A": {
"aggregat": ["S3", "S4"],
"ordre": 6,
"affichage_court": "2A",
"affichage_long": "2ème année",
},
"3S": {
"aggregat": ["S1", "S2", "S3"],
"ordre": 7,
"affichage_court": "S1+S2+S3",
"affichage_long": "BUT du semestre 1 au semestre 3",
},
"4S": {
"aggregat": ["S1", "S2", "S3", "S4"],
"ordre": 8,
"affichage_court": "BUT",
"affichage_long": "BUT du semestre 1 au semestre 4",
},
"S5": {
"aggregat": ["S5"],
"ordre": 9,
"affichage_court": "S5",
"affichage_long": "Semestre 5",
},
"S6": {
"aggregat": ["S6"],
"ordre": 10,
"affichage_court": "S6",
"affichage_long": "Semestre 6",
},
"3A": {
"aggregat": ["S5", "S6"],
"ordre": 11,
"affichage_court": "3A",
"affichage_long": "3ème année",
},
"5S": {
"aggregat": ["S1", "S2", "S3", "S4", "S5"],
"ordre": 12,
"affichage_court": "S1+S2+S3+S4+S5",
"affichage_long": "BUT du semestre 1 au semestre 5",
},
"6S": {
"aggregat": ["S1", "S2", "S3", "S4", "S5", "S6"],
"ordre": 13,
"affichage_court": "BUT",
"affichage_long": "BUT (tout semestre inclus)",
},
}
NBRE_SEMESTRES_DIPLOMANT = 6
AGGREGAT_DIPLOMANT = (
"6S" # aggrégat correspondant à la totalité des notes pour le diplôme
)
TOUS_LES_SEMESTRES = PARCOURS[AGGREGAT_DIPLOMANT]["aggregat"]
TOUS_LES_AGGREGATS = [cle for cle in PARCOURS.keys() if not cle.startswith("S")]
TOUS_LES_PARCOURS = list(PARCOURS.keys())
# ----------------------------------------------------------------------------------------
def calcul_age(born: datetime.date) -> int:
"""Calcule l'age connaissant la date de naissance ``born``. (L'age est calculé
à partir de l'horloge système).
Args:
born: La date de naissance
Return:
L'age (au regard de la date actuelle)
"""
if not born or not isinstance(born, datetime.date):
return None
today = datetime.date.today()
return (
today.year
- born.year
- ((today.month, today.day) < (born.month, born.day))
)
def remove_accents(input_unicode_str):
"""Supprime les accents d'une chaine unicode"""
nfkd_form = unicodedata.normalize("NFKD", input_unicode_str)
only_ascii = nfkd_form.encode("ASCII", "ignore")
return only_ascii
def escape_for_latex(s):
"""Protège les caractères pour inclusion dans du source LaTeX"""
if not s:
return ""
conv = {
"&": r"\&",
"%": r"\%",
"$": r"\$",
"#": r"\#",
"_": r"\_",
"{": r"\{",
"}": r"\}",
"~": r"\textasciitilde{}",
"^": r"\^{}",
"\\": r"\textbackslash{}",
"<": r"\textless ",
">": r"\textgreater ",
}
exp = re.compile(
"|".join(
re.escape(key)
for key in sorted(list(conv.keys()), key=lambda item: -len(item))
)
)
return exp.sub(lambda match: conv[match.group()], s)
# ----------------------------------------------------------------------------------------
def list_directory_filenames(path):
"""List of regular filenames in a directory (recursive)
Excludes files and directories begining with .
"""
R = []
for root, dirs, files in os.walk(path, topdown=True):
dirs[:] = [d for d in dirs if d[0] != "."]
R += [os.path.join(root, fn) for fn in files if fn[0] != "."]
return R
def add_local_file_to_zip(zipfile, ziproot, pathname, path_in_zip):
"""Read pathname server file and add content to zip under path_in_zip"""
rooted_path_in_zip = os.path.join(ziproot, path_in_zip)
zipfile.write(filename=pathname, arcname=rooted_path_in_zip)
# data = open(pathname).read()
# zipfile.writestr(rooted_path_in_zip, data)
def add_refs_to_register(register, directory):
"""Ajoute les fichiers trouvés dans directory au registre (dictionaire) sous la forme
filename => pathname
"""
length = len(directory)
for pathname in list_directory_filenames(directory):
filename = pathname[length + 1 :]
register[filename] = pathname
def add_pe_stuff_to_zip(zipfile, ziproot):
"""Add auxiliary files to (already opened) zip
Put all local files found under config/doc_poursuites_etudes/local
and config/doc_poursuites_etudes/distrib
If a file is present in both subtrees, take the one in local.
Also copy logos
"""
register = {}
# first add standard (distrib references)
distrib_dir = os.path.join(REP_DEFAULT_AVIS, "distrib")
add_refs_to_register(register=register, directory=distrib_dir)
# then add local references (some oh them may overwrite distrib refs)
local_dir = os.path.join(REP_LOCAL_AVIS, "local")
add_refs_to_register(register=register, directory=local_dir)
# at this point register contains all refs (filename, pathname) to be saved
for filename, pathname in register.items():
add_local_file_to_zip(zipfile, ziproot, pathname, "avis/" + filename)
# Logos: (add to logos/ directory in zip)
logos_names = ["header", "footer"]
for name in logos_names:
logo = find_logo(logoname=name, dept_id=g.scodoc_dept_id)
if logo is not None:
add_local_file_to_zip(
zipfile, ziproot, logo.filepath, "avis/logos/" + logo.filename
)
# ----------------------------------------------------------------------------------------
def get_annee_diplome_semestre(sem_base, nbre_sem_formation=6) -> int:
"""Pour un semestre ``sem_base`` donné (supposé être un semestre d'une formation BUT à 6 semestres)
et connaissant le numéro du semestre, ses dates de début et de fin du semestre, prédit l'année à laquelle
sera remis le diplôme BUT des étudiants qui y sont scolarisés
(en supposant qu'il n'y ait pas de redoublement à venir).
**Remarque sur le calcul** : Les semestres de 1ère partie d'année (S1, S3, S5 ou S4, S6 pour des semestres décalés)
s'étalent sur deux années civiles ; contrairement au semestre de seconde partie d'année universitaire.
Par exemple :
* S5 débutant en 2025 finissant en 2026 : diplome en 2026
* S3 debutant en 2025 et finissant en 2026 : diplome en 2027
La fonction est adaptée au cas des semestres décalés.
Par exemple :
* S5 décalé débutant en 2025 et finissant en 2025 : diplome en 2026
* S3 décalé débutant en 2025 et finissant en 2025 : diplome en 2027
Args:
sem_base: Le semestre à partir duquel est prédit l'année de diplomation, soit :
* un ``FormSemestre`` (Scodoc9)
* un dict (format compatible avec Scodoc7)
nbre_sem_formation: Le nombre de semestre prévu dans la formation (par défaut 6 pour un BUT)
"""
if isinstance(sem_base, FormSemestre):
sem_id = sem_base.semestre_id
annee_fin = sem_base.date_fin.year
annee_debut = sem_base.date_debut.year
else: # sem_base est un dictionnaire (Scodoc 7)
sem_id = sem_base["semestre_id"]
annee_fin = int(sem_base["annee_fin"])
annee_debut = int(sem_base["annee_debut"])
if (
1 <= sem_id <= nbre_sem_formation
): # Si le semestre est un semestre BUT => problème si formation BUT en 1 an ??
nbreSemRestant = (
nbre_sem_formation - sem_id
) # nombre de semestres restant avant diplome
nbreAnRestant = nbreSemRestant // 2 # nombre d'annees restant avant diplome
# Flag permettant d'activer ou désactiver un increment à prendre en compte en cas de semestre décalé
# avec 1 - delta = 0 si semestre de 1ere partie d'année / 1 sinon
delta = annee_fin - annee_debut
decalage = nbreSemRestant % 2 # 0 si S4, 1 si S3, 0 si S2, 1 si S1
increment = decalage * (1 - delta)
return annee_fin + nbreAnRestant + increment
def get_cosemestres_diplomants(annee_diplome: int, formation_id: int) -> list:
"""Ensemble des cosemestres donnant lieu à diplomation à l'``annee_diplome``
et s'intégrant à la formation donnée par son ``formation_id``.
**Définition** : Un co-semestre est un semestre :
* dont l'année de diplômation prédite (sans redoublement) est la même
* dont la formation est la même (optionnel)
* qui a des étudiants inscrits
Si formation_id == None, ne prend pas en compte l'identifiant de formation
TODO:: A raccrocher à un programme
Args:
annee_diplome: L'année de diplomation
formation_id: L'identifiant de la formation
"""
tousLesSems = (
sco_formsemestre.do_formsemestre_list()
) # tous les semestres memorisés dans scodoc
if formation_id:
cosemestres_fids = {
sem["id"]
for sem in tousLesSems
if get_annee_diplome_semestre(sem) == annee_diplome
and sem["formation_id"] == formation_id
}
else:
cosemestres_fids = {
sem["id"]
for sem in tousLesSems
if get_annee_diplome_semestre(sem) == annee_diplome
}
cosemestres = {}
for fid in cosemestres_fids:
cosem = FormSemestre.get_formsemestre(fid)
if len(cosem.etuds_inscriptions) > 0:
cosemestres[fid] = cosem
return cosemestres

527
app/pe/pe_etudiant.py Normal file
View File

@ -0,0 +1,527 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
##############################################################################
# Module "Avis de poursuite d'étude"
# conçu et développé par Cléo Baras (IUT de Grenoble)
##############################################################################
"""
Created on 17/01/2024
@author: barasc
"""
import app.pe.pe_comp as pe_comp
from app.models import FormSemestre, Identite
from app.pe.pe_comp import pe_print
class EtudiantsJuryPE:
"""Classe centralisant la gestion des étudiants à prendre en compte dans un jury de PE"""
def __init__(self, annee_diplome: int):
"""
Args:
annee_diplome: L'année de diplomation
"""
self.annee_diplome = annee_diplome
"Les identités des étudiants traités pour le jury"
self.identites = {} # ex. ETUDINFO_DICT
"Les cursus (semestres suivis, abandons) des étudiants"
self.cursus = {}
"""Les aggrégats des semestres suivis (par ex: 3S=S1+S2+S3 à prendre en compte avec d'éventuels redoublements) des étudiants"""
self.trajectoires = {}
"Les etudids des étudiants à considérer au jury (ceux qui seront effectivement diplômés)"
self.etudiants_diplomes = {}
self.diplomes_ids = {}
"Les etudids des étudiants dont il faut calculer les moyennes/classements (même si d'éventuels abandons)"
self.etudiants_ids = {}
def find_etudiants(self, formation_id: int):
"""Liste des étudiants à prendre en compte dans le jury PE, en les recherchant
de manière automatique par rapport à leur année de diplomation ``annee_diplome``
dans la formation ``formation_id``.
Les données obtenues sont stockées dans les attributs de EtudiantsJuryPE.
formation_id: L'identifiant de la formation (inutilisé)
*Remarque* : ex: JuryPE.get_etudiants_in_jury()
"""
"Les cosemestres donnant lieu à même année de diplome"
cosemestres = pe_comp.get_cosemestres_diplomants(self.annee_diplome, None)
self.cosemestres = cosemestres
pe_comp.pe_print(
"1) Recherche des coSemestres -> %d trouvés" % len(cosemestres)
)
"""Les étudiants inscrits dans les co-semestres (ceux du jury mais aussi d'autres ayant été réorientés ou ayant abandonnés)"""
pe_comp.pe_print("2) Liste des étudiants dans les différents co-semestres")
self.etudiants_ids = get_etudiants_dans_semestres(cosemestres)
pe_comp.pe_print(
" => %d étudiants trouvés dans les cosemestres" % len(self.etudiants_ids)
)
"""Analyse des parcours étudiants pour déterminer leur année effective de diplome
avec prise en compte des redoublements, des abandons, ...."""
pe_comp.pe_print("3) Analyse des parcours individuels des étudiants")
no_etud = 0
for no_etud, etudid in enumerate(self.etudiants_ids):
"""L'identité de l'étudiant"""
identite = Identite.get_etud(etudid)
self.identites[etudid] = identite
"""L'analyse de son cursus"""
self.analyse_etat_etudiant(etudid, cosemestres)
"""L'analyse de son parcours pour atteindre chaque semestre de la formation"""
self.structure_cursus_etudiant(etudid)
if (no_etud + 1) % 10 == 0:
pe_comp.pe_print(f"{no_etud + 1}")
no_etud += 1
pe_comp.pe_print()
"""Les étudiants à prendre dans le diplôme, étudiants ayant abandonnés non compris"""
self.etudiants_diplomes = self.get_etudiants_diplomes()
self.diplomes_ids = set(self.etudiants_diplomes.keys())
"""Les étudiants dont il faut calculer les moyennes"""
self.etudiants_ids = {etudid for etudid in self.identites}
"""Les formsemestres (des étudiants) dont il faut calculer les moyennes"""
self.formsemestres_jury_ids = self.get_formsemestres()
# Synthèse
pe_comp.pe_print(
f" => {len(self.etudiants_diplomes)} étudiants à diplômer en {self.annee_diplome}"
)
nbre_abandons = len(self.etudiants_ids) - len(self.etudiants_diplomes)
pe_comp.pe_print(f" => {nbre_abandons} étudiants éliminer pour abandon")
pe_comp.pe_print(
f" => {len(self.formsemestres_jury_ids)} semestres dont il faut calculer la moyenne"
)
pe_comp.pe_print(
f" => quelques étudiants futurs diplômés : "
+ ", ".join([str(etudid) for etudid in list(self.etudiants_diplomes)[:10]])
)
pe_comp.pe_print(
f" => semestres dont il faut calculer les moyennes : "
+ ", ".join([str(fid) for fid in list(self.formsemestres_jury_ids)])
)
# Les abandons :
self.abandons = sorted([self.cursus[etudid]['nom']
for etudid in self.cursus if etudid not in self.diplomes_ids])
def get_etudiants_diplomes(self) -> dict[int, Identite]:
"""Identités des étudiants (sous forme d'un dictionnaire `{etudid: Identite(etudid)}`
qui vont être à traiter au jury PE pour
l'année de diplômation donnée et n'ayant ni été réorienté, ni abandonné.
Returns:
Un dictionnaire `{etudid: Identite(etudid)}`
"""
etudids = [
etudid
for etudid in self.cursus
if self.cursus[etudid]["diplome"] == self.annee_diplome
and self.cursus[etudid]["abandon"] == False
]
etudiants = {etudid: self.identites[etudid] for etudid in etudids}
return etudiants
def analyse_etat_etudiant(self, etudid: int, cosemestres: dict[int, FormSemestre]):
"""Analyse le cursus d'un étudiant pouvant être :
* l'un de ceux sur lesquels le jury va statuer (année de diplômation du jury considéré)
* un étudiant qui ne sera pas considéré dans le jury mais qui a participé dans sa scolarité
à un (ou plusieurs) semestres communs aux étudiants du jury (et impactera les classements)
L'analyse consiste :
* à insérer une entrée dans ``self.cursus`` pour mémoriser son identité,
avec son nom, prénom, etc...
* à analyser son parcours, pour déterminer s'il n'a (ou non) abandonné l'IUT en cours de
route (cf. clé abandon)
Args:
etudid: L'etudid d'un étudiant, à ajouter à ceux traiter par le jury
cosemestres: Dictionnaire {fid: Formsemestre(fid)} donnant accès aux cosemestres
de même année de diplomation
"""
identite = Identite.get_etud(etudid)
"""Le cursus global de l'étudiant (restreint aux semestres APC)"""
formsemestres = identite.get_formsemestres()
semestres_etudiant = {
frmsem.formsemestre_id: frmsem
for frmsem in formsemestres
if frmsem.formation.is_apc()
}
self.cursus[etudid] = {
"etudid": etudid, # les infos sur l'étudiant
"etat_civil": identite.etat_civil, # Ajout à la table jury
"nom": identite.nom,
"entree": formsemestres[-1].date_debut.year, # La date d'entrée à l'IUT
"diplome": annee_diplome(identite), # Le date prévisionnelle de son diplôme
"formsemestres": semestres_etudiant, # les semestres de l'étudiant
"nb_semestres": len(semestres_etudiant), # le nombre de semestres de l'étudiant
"abandon": False, # va être traité en dessous
}
""" Est-il réorienté / démissionnaire ou a-t-il arrêté volontairement sa formation ?"""
self.cursus[etudid]["abandon"] = arret_de_formation(identite, cosemestres)
def get_semestres_significatifs(self, etudid: int):
"""Ensemble des semestres d'un étudiant, qui l'auraient amené à être diplomé
l'année visée (supprime les semestres qui conduisent à une diplomation
postérieure à celle du jury visé)
Args:
etudid: L'identifiant d'un étudiant
Returns:
Un dictionnaire ``{fid: FormSemestre(fid)`` dans lequel les semestres
amènent à une diplomation avant l'annee de diplomation du jury
"""
semestres_etudiant = self.cursus[etudid]["formsemestres"]
semestres_significatifs = {}
for fid in semestres_etudiant:
semestre = semestres_etudiant[fid]
if pe_comp.get_annee_diplome_semestre(semestre) <= self.annee_diplome:
semestres_significatifs[fid] = semestre
return semestres_significatifs
def structure_cursus_etudiant(self, etudid: int):
"""Structure les informations sur les semestres suivis par un
étudiant, pour identifier les semestres qui seront pris en compte lors de ses calculs
de moyennes PE.
Cette structuration s'appuie sur les numéros de semestre: pour chaque Si, stocke :
le dernier semestre (en date) de numéro i qu'il a suivi (1 ou 0 si pas encore suivi). Ce semestre influera les
interclassement par semestre dans la promo.
"""
semestres_significatifs = self.get_semestres_significatifs(etudid)
"""Tri des semestres par numéro de semestre"""
for nom_sem in pe_comp.TOUS_LES_SEMESTRES:
i = int(nom_sem[1]) # le n° du semestre
semestres_i = {
fid: semestres_significatifs[fid]
for fid in semestres_significatifs
if semestres_significatifs[fid].semestre_id == i
} # les semestres de n°i de l'étudiant
self.cursus[etudid][nom_sem] = semestres_i
def get_trajectoire(self, etudid: int, formsemestre_final: FormSemestre):
"""Ensemble des semestres parcourus par
un étudiant pour l'amener à un semestre terminal.
Par ex: si formsemestre_terminal est un S3, ensemble des S1,
S2, S3 suivi pour l'amener au S3 (il peut y avoir plusieurs S1,
ou S2, ou S3 s'il a redoublé).
Les semestres parcourus sont antérieurs (en terme de date de fin)
au formsemestre_terminal.
Args:
etudid: L'identifiant de l'étudiant
formsemestre_final: le semestre final visé
"""
numero_semestre_terminal = formsemestre_final.semestre_id
semestres_significatifs = self.get_semestres_significatifs(etudid)
"""Semestres de n° inférieur (pax ex: des S1, S2, S3 pour un S3 terminal) et qui lui sont antérieurs"""
semestres_aggreges = {}
for fid in semestres_significatifs:
semestre = semestres_significatifs[fid]
if (
semestre.semestre_id <= numero_semestre_terminal
and semestre.date_fin <= formsemestre_final.date_fin
):
semestres_aggreges[fid] = semestre
return semestres_aggreges
def get_formsemestres_terminaux_aggregat(self, aggregat: str):
"""Pour un aggrégat donné, ensemble des formsemestres terminaux possibles pour l'aggrégat
(pour l'aggrégat '3S' incluant S1+S2+S3, a pour semestre terminal S3).
Ces formsemestres traduisent :
* les différents parcours des étudiants liés par exemple au choix de modalité (par ex: S1 FI + S2 FI + S3 FI
ou S1 FI + S2 FI + S3 UFA), en renvoyant les formsemestre_id du S3 FI et du S3 UFA.
* les éventuelles situations de redoublement (par ex pour 1 étudiant ayant redoublé sa 2ème année :
S1 + S2 + S3 (1ère session) et S1 + S2 + S3 + S4 + S3 (2ème session), en renvoyant les formsemestre_id du
S3 (1ère session) et du S3 (2ème session)
Args:
aggregat: L'aggrégat
Returns:
Un dictionnaire ``{fid: FormSemestre(fid)}``
"""
formsemestres_terminaux = {}
for etudid in self.trajectoires:
if self.trajectoires[etudid][aggregat]:
trajectoire = self.trajectoires[etudid][aggregat]
"""Le semestre terminal de l'étudiant de l'aggrégat"""
fid = trajectoire.semestre_final.formsemestre_id
formsemestres_terminaux[fid] = trajectoire.semestre_final
return formsemestres_terminaux
def get_formsemestres(self, semestres_recherches=None):
"""Ayant connaissance des étudiants dont il faut calculer les moyennes pour
le jury PE (attribut `self.etudiant_ids) et de leur cursus (semestres
parcourus),
renvoie un dictionnaire ``{fid: FormSemestre(fid)}``
contenant l'ensemble des formsemestres de leurs cursus, dont il faudra calculer
la moyenne.
Les formsemestres sont limités à ceux indiqués dans ``semestres_recherches``.
Args:
semestres_recherches: Une liste ou une chaine de caractères parmi :
* None : pour obtenir tous les formsemestres du jury
* 'Si' : pour obtenir les semestres de n° i (par ex. 'S1')
* 'iA' : pour obtenir les semestres de l'année i (par ex. '1A' donne ['S1, 'S2'])
* '3S', '4S' : pour obtenir les combinaisons de semestres définies par les aggrégats
Returns:
Un dictionnaire de la forme ``{fid: FormSemestre(fid)}``
Remarque:
Une liste de la forme ``[ 'Si', 'iA' , ... ]`` (combinant les formats précédents) est possible.
"""
if semestres_recherches is None:
"""Appel récursif pour obtenir tous les semestres (validants)"""
semestres = self.get_formsemestres(pe_comp.AGGREGAT_DIPLOMANT)
return semestres
elif isinstance(semestres_recherches, list):
"""Appel récursif sur tous les éléments de la liste"""
semestres = {}
for elmt in semestres_recherches:
semestres_elmt = self.get_formsemestres(elmt)
semestres = semestres | semestres_elmt
return semestres
elif (
isinstance(semestres_recherches, str)
and semestres_recherches in pe_comp.TOUS_LES_AGGREGATS
):
"""Cas d'un aggrégat avec appel récursif sur toutes les entrées de l'aggrégat"""
semestres = self.get_formsemestres(
pe_comp.PARCOURS[semestres_recherches]["aggregat"]
)
return semestres
elif (
isinstance(semestres_recherches, str)
and semestres_recherches in pe_comp.TOUS_LES_SEMESTRES
):
"""semestres_recherches est un nom de semestre de type S1,
pour une recherche parmi les étudiants à prendre en compte
dans le jury (diplômé et redoublants non diplômé)
"""
nom_sem = semestres_recherches
semestres = {}
for etudid in self.etudiants_ids:
if self.cursus[etudid][nom_sem]:
semestres = semestres | self.cursus[etudid][nom_sem]
return semestres
else:
raise ValueError("Probleme de paramètres d'appel dans get_formsemestreids")
def nbre_etapes_max_diplomes(self):
"""Connaissant les étudiants diplomes du jury PE,
nombre de semestres (étapes) maximum suivis par les étudiants du jury.
"""
nbres_semestres = []
for etudid in self.diplomes_ids:
nbres_semestres.append( self.cursus[etudid]["nb_semestres"] )
return max(nbres_semestres)
def get_etudiants_dans_semestres(semestres: dict[int, FormSemestre]) -> set:
"""Ensemble d'identifiants des étudiants (identifiés via leur ``etudid``)
inscrits à l'un des semestres de la liste de ``semestres``.
Remarque : Les ``cosemestres`` sont généralement obtenus avec ``sco_formsemestre.do_formsemestre_list()``
Args:
semestres: Un dictionnaire ``{fid: Formsemestre(fid)}`` donnant un
ensemble d'identifiant de semestres
Returns:
Un ensemble d``etudid``
"""
etudiants_ids = set()
for fid, sem in semestres.items(): # pour chacun des semestres de la liste
etudiants_du_sem = {ins.etudid for ins in sem.inscriptions}
pe_print(f" --> {sem} : {len(etudiants_du_sem)} etudiants")
etudiants_ids = (
etudiants_ids | etudiants_du_sem
) # incluant la suppression des doublons
return etudiants_ids
def annee_diplome(identite: Identite) -> int:
"""L'année de diplôme prévue d'un étudiant en fonction de ses semestres
d'inscription (pour un BUT).
Args:
identite: L'identité d'un étudiant
Returns:
L'année prévue de sa diplômation
NOTE: Pourrait être déplacé dans app.models.etudiants.Identite
"""
formsemestres = identite.get_formsemestres()
if formsemestres:
return max(
[
pe_comp.get_annee_diplome_semestre(sem_base)
for sem_base in formsemestres
]
)
else:
return None
def arret_de_formation(identite: Identite, cosemestres: list[FormSemestre]) -> bool:
"""Détermine si un étudiant a arrêté sa formation. Il peut s'agir :
* d'une réorientation à l'initiative du jury de semestre ou d'une démission (on pourrait
utiliser les code NAR pour réorienté & DEM pour démissionnaire des résultats du jury renseigné dans la BDD,
mais pas nécessaire ici)
* d'un arrêt volontaire : l'étudiant disparait des listes d'inscrits (sans pour autant avoir été indiqué NAR ou DEM).
Dans les cas, on considérera que l'étudiant a arrêté sa formation s'il n'est pas dans l'un des "derniers" cosemestres
(semestres conduisant à la même année de diplômation) connu dans Scodoc.
Par ex: au moment du jury PE en fin de S5 (pas de S6 renseigné dans Scodoc), l'étudiant doit appartenir à une
instance des S5 qui conduisent à la diplomation dans l'année visée. S'il n'est que dans un S4, il a sans doute
arrêté. A moins qu'il ne soit parti à l'étranger et , pas de notes.
TODO:: Cas de l'étranger, à coder/tester
**Attention** : Cela suppose que toutes les instances d'un semestre donné (par ex: toutes les instances de S6
accueillant un étudiant soient créées ; sinon les étudiants non inscrits dans un S6 seront considérés comme
ayant abandonnés)
TODO:: Peut-être à mettre en regard avec les propositions d'inscriptions d'étudiants dans un nouveau semestre
Pour chaque étudiant, recherche son dernier semestre en date (validé ou non) et
regarde s'il n'existe pas parmi les semestres existants dans Scodoc un semestre :
* dont les dates sont postérieures (en terme de date de début)
* de n° au moins égal à celui de son dernier semestre valide (S5 -> S5 ou S5 -> S6)
dans lequel il aurait pu s'inscrire mais ne l'a pas fait.
Args:
identite: L'identité d'un étudiant
cosemestres: Les semestres donnant lieu à diplômation (sans redoublement) en date du jury
Returns:
Est-il réorienté, démissionnaire ou a-t-il arrêté de son propre chef sa formation ?
TODO:: A reprendre pour le cas des étudiants à l'étranger
TODO:: A reprendre si BUT avec semestres décalés
"""
etudid = identite.etudid
"""Son dernier semestre en date"""
semestres = {sem.semestre_id: sem for sem in identite.get_formsemestres()}
dernier_formsemestre = get_dernier_semestre_en_date(semestres)
numero_dernier_formsemestre = dernier_formsemestre.semestre_id
"""Les numéro de semestres possible dans lesquels il pourrait s'incrire"""
# semestre impair => passage de droit en semestre pair suivant (effet de l'annualisation)
if numero_dernier_formsemestre % 2 == 1:
numeros_possibles = list(
range(numero_dernier_formsemestre + 1, pe_comp.NBRE_SEMESTRES_DIPLOMANT)
)
# semestre pair => passage en année supérieure ou redoublement
else: #
numeros_possibles = list(
range(
max(numero_dernier_formsemestre - 1, 1),
pe_comp.NBRE_SEMESTRES_DIPLOMANT,
)
)
"""Y-a-t-il des cosemestres dans lesquels il aurait pu s'incrire ?"""
formsestres_superieurs_possibles = []
for fid, sem in cosemestres.items(): # Les semestres ayant des inscrits
if (
fid != dernier_formsemestre.formsemestre_id
and sem.semestre_id in numeros_possibles
and sem.date_debut.year >= dernier_formsemestre.date_debut.year
): # date de debut des semestres possibles postérieur au dernier semestre de l'étudiant et de niveau plus élevé que le dernier semestre valide de l'étudiant
formsestres_superieurs_possibles.append(fid)
if len(formsestres_superieurs_possibles) > 0:
return True
return False
def get_dernier_semestre_en_date(semestres: dict[int, FormSemestre]) -> FormSemestre:
"""Renvoie le dernier semestre en **date de fin** d'un dictionnaire
de semestres (potentiellement non trié) de la forme ``{fid: FormSemestre(fid)}``.
Args:
semestres: Un dictionnaire de semestres
Return:
Le FormSemestre du semestre le plus récent
"""
if semestres:
fid_dernier_semestre = list(semestres.keys())[0]
dernier_semestre: FormSemestre = semestres[fid_dernier_semestre]
for fid in semestres:
if semestres[fid].date_fin > dernier_semestre.date_fin:
dernier_semestre = semestres[fid]
return dernier_semestre
else:
return None

125
app/pe/pe_interclasstag.py Normal file
View File

@ -0,0 +1,125 @@
from app.pe.pe_tabletags import TableTag
from app.pe.pe_etudiant import EtudiantsJuryPE
from app.pe.pe_trajectoire import Trajectoire, TrajectoiresJuryPE
from app.pe.pe_trajectoiretag import TrajectoireTag
from app.comp import moy_sem
import pandas as pd
import numpy as np
class AggregatInterclasseTag(TableTag):
"""Interclasse l'ensemble des étudiants diplômés à une année
donnée (celle du jury), pour un aggrégat donné (par ex: 'S2', '3S')
en reportant :
* les moyennes obtenues sur la trajectoire qu'il ont suivi pour atteindre le numéro de semestre de fin de l'aggrégat (indépendamment de son
formsemestres)
* calculant le classement sur les étudiants diplômes
"""
# -------------------------------------------------------------------------------------------------------------------
def __init__(
self,
nom_aggregat: str,
etudiants: EtudiantsJuryPE,
trajectoires_jury_pe: TrajectoiresJuryPE,
trajectoires_taggues: dict[tuple, TrajectoireTag],
):
""""""
"""Table nommée au nom de l'aggrégat (par ex: 3S"""
TableTag.__init__(self, nom_aggregat)
"""Les étudiants diplômés et leurs trajectoires (cf. trajectoires.suivis)"""
self.diplomes_ids = etudiants.etudiants_diplomes
self.etudiants_diplomes = {etudid for etudid in self.diplomes_ids}
"""Les trajectoires (et leur version tagguées), en ne gardant que celles associées à l'aggrégat
"""
self.trajectoires: dict[int, Trajectoire] = {}
for trajectoire_id in trajectoires_jury_pe.trajectoires:
trajectoire = trajectoires_jury_pe.trajectoires[trajectoire_id]
if trajectoire_id[0] == nom_aggregat:
self.trajectoires[trajectoire_id] = trajectoire
self.trajectoires_taggues: dict[int, Trajectoire] = {}
for trajectoire_id in self.trajectoires:
self.trajectoires_taggues[trajectoire_id] = trajectoires_taggues[
trajectoire_id
]
"""Les trajectoires suivies par les étudiants du jury, en ne gardant que
celles associées aux diplomés"""
self.suivi: dict[int, Trajectoire] = {}
for etudid in self.diplomes_ids:
self.suivi[etudid] = trajectoires_jury_pe.suivi[etudid][nom_aggregat]
"""Les tags"""
self.tags_sorted = self.do_taglist()
"""Construit la matrice de notes"""
self.notes = self.compute_notes_matrice()
"""Synthétise les moyennes/classements par tag"""
self.moyennes_tags = {}
for tag in self.tags_sorted:
moy_gen_tag = self.notes[tag]
class_gen_tag = moy_sem.comp_ranks_series(moy_gen_tag)[1] # en int
self.moyennes_tags[tag] = {
"notes": moy_gen_tag,
"classements": class_gen_tag,
"min": moy_gen_tag.min(),
"max": moy_gen_tag.max(),
"moy": moy_gen_tag.mean(),
"nb_inscrits": len(moy_gen_tag),
}
def get_repr(self) -> str:
"""Une représentation textuelle"""
return f"Aggrégat {self.nom}"
def do_taglist(self):
"""Synthétise les tags à partir des trajectoires_tagguées
Returns:
Une liste de tags triés par ordre alphabétique
"""
tags = []
for trajectoire_id in self.trajectoires_taggues:
trajectoire = self.trajectoires_taggues[trajectoire_id]
tags.extend(trajectoire.tags_sorted)
return sorted(set(tags))
def compute_notes_matrice(self):
"""Construit la matrice de notes (etudid x tags)
retraçant les moyennes obtenues par les étudiants dans les semestres associés à
l'aggrégat (une trajectoire ayant pour numéro de semestre final, celui de l'aggrégat).
"""
nb_tags = len(self.tags_sorted)
nb_etudiants = len(self.diplomes_ids)
"""Index de la matrice (etudids -> dim 0, tags -> dim 1)"""
etudids = list(self.diplomes_ids)
tags = self.tags_sorted
"""Partant d'un dataframe vierge"""
df = pd.DataFrame(np.nan, index=etudids, columns=tags)
for trajectoire_id in self.trajectoires_taggues:
"""Charge les moyennes par tag de la trajectoire tagguée"""
notes = self.trajectoires_taggues[trajectoire_id].notes
"""Etudiants/Tags communs entre la trajectoire_tagguée et les données interclassées"""
etudids_communs = df.index.intersection(notes.index)
tags_communs = df.columns.intersection(notes.columns)
"""Injecte les notes par tag"""
df.loc[etudids_communs, tags_communs] = notes.loc[
etudids_communs, tags_communs
]
return df

480
app/pe/pe_jury.py Normal file
View File

@ -0,0 +1,480 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
##############################################################################
# Module "Avis de poursuite d'étude"
# conçu et développé par Cléo Baras (IUT de Grenoble)
##############################################################################
"""
Created on Fri Sep 9 09:15:05 2016
@author: barasc
"""
# ----------------------------------------------------------
# Ensemble des fonctions et des classes
# permettant les calculs preliminaires (hors affichage)
# a l'edition d'un jury de poursuites d'etudes
# ----------------------------------------------------------
import io
import os
from zipfile import ZipFile
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre
from app.models.etudiants import Identite
from app.scodoc.gen_tables import GenTable, SeqGenTable
import app.scodoc.sco_utils as scu
from app.pe.pe_etudiant import EtudiantsJuryPE
from app.pe.pe_trajectoire import TrajectoiresJuryPE, Trajectoire
import app.pe.pe_comp as pe_comp
from app.pe.pe_semtag import SemestreTag
from app.pe.pe_interclasstag import AggregatInterclasseTag
from app.pe.pe_trajectoiretag import TrajectoireTag
import app.pe.pe_affichage as pe_affichage
import pandas as pd
import numpy as np
# ----------------------------------------------------------------------------------------
# ----------------------------------------------------------------------------------------
class JuryPE(object):
"""Classe mémorisant toutes les informations nécessaires pour établir un jury de PE.
Modèle basé sur NotesTable.
Attributs :
* diplome : l'année d'obtention du diplome BUT et du jury de PE (généralement février XXXX)
* juryEtudDict : dictionnaire récapitulant les étudiants participant au jury PE (données administratives +
celles des semestres valides à prendre en compte permettant le calcul des moyennes ...
``{'etudid : { 'nom', 'prenom', 'civilite', 'diplome', '', }}``
a
Rq: il contient à la fois les étudiants qui vont être diplomés à la date prévue
et ceux qui sont éliminés (abandon, redoublement, ...) pour affichage alternatif
"""
# Variables de classe décrivant les aggrégats, leur ordre d'apparition temporelle et
# leur affichage dans les avis latex
# ------------------------------------------------------------------------------------------------------------------
def __init__(self, diplome, formation_id):
"""
Création d'une table PE sur la base d'un semestre selectionné. De ce semestre est déduit :
1. l'année d'obtention du DUT,
2. tous les étudiants susceptibles à ce stade (au regard de leur parcours) d'être diplomés.
Args:
sem_base: le FormSemestre donnant le semestre à la base du jury PE
semBase: le dictionnaire sem donnant la base du jury (CB: TODO: A supprimer à long term)
meme_programme: si True, impose un même programme pour tous les étudiants participant au jury,
si False, permet des programmes differents
"""
"L'année du diplome"
self.diplome = diplome
"La formation associée au diplome"
self.formation_id = formation_id
"Un zip où ranger les fichiers générés"
self.nom_export_zip = "Jury_PE_%s" % self.diplome
self.zipdata = io.BytesIO()
self.zipfile = ZipFile(self.zipdata, "w")
"""Chargement des étudiants à prendre en compte dans le jury"""
pe_comp.pe_print(
f"*** Recherche et chargement des étudiants diplômés en {self.diplome} pour la formation {self.formation_id}"
)
self.etudiants = EtudiantsJuryPE(self.diplome) # Les infos sur les étudiants
self.etudiants.find_etudiants(self.formation_id)
self.diplomes_ids = self.etudiants.diplomes_ids
"""Génère les semestres taggués (avec le calcul des moyennes) pour le jury PE"""
pe_comp.pe_print("*** Génère les semestres taggués")
self.semestres_taggues = compute_semestres_tag(self.etudiants)
if pe_comp.PE_DEBUG:
"""Intègre le bilan des semestres taggués au zip final"""
for fid in self.semestres_taggues:
formsemestretag = self.semestres_taggues[fid]
filename = formsemestretag.nom.replace(" ", "_") + ".csv"
pe_comp.pe_print(f" - Export csv de {filename} ")
self.add_file_to_zip(
filename, formsemestretag.str_tagtable(), path="details_semestres"
)
"""Génère les trajectoires (combinaison de semestres suivis
par un étudiant pour atteindre le semestre final d'un aggrégat)
"""
pe_comp.pe_print(
"*** Génère les trajectoires (différentes combinaisons de semestres) des étudiants"
)
self.trajectoires = TrajectoiresJuryPE(self.diplome)
self.trajectoires.cree_trajectoires(self.etudiants)
"""Génère les moyennes par tags des trajectoires"""
pe_comp.pe_print("*** Calcule les moyennes par tag des trajectoires possibles")
self.trajectoires_tagguees = compute_trajectoires_tag(
self.trajectoires, self.etudiants, self.semestres_taggues
)
if pe_comp.PE_DEBUG:
"""Intègre le bilan des trajectoires tagguées au zip final"""
for trajectoire_id in self.trajectoires_tagguees:
trajectoire_tagguee = self.trajectoires_tagguees[trajectoire_id]
filename = trajectoire_tagguee.get_repr().replace(" ", "_") + ".csv"
pe_comp.pe_print(f" - Export csv de {filename} ")
self.add_file_to_zip(
filename,
trajectoire_tagguee.str_tagtable(),
path="details_semestres",
)
"""Génère les interclassements (par promo et) par (nom d') aggrégat"""
pe_comp.pe_print("*** Génère les interclassements par aggrégat")
self.interclassements_taggues = compute_interclassements(
self.etudiants, self.trajectoires, self.trajectoires_tagguees
)
if pe_comp.PE_DEBUG:
"""Intègre le bilan des aggrégats (par promo) au zip final"""
for nom_aggregat in self.interclassements_taggues:
interclass_tag = self.interclassements_taggues[nom_aggregat]
filename = interclass_tag.get_repr().replace(" ", "_") + ".csv"
pe_comp.pe_print(f" - Export csv de {filename} ")
self.add_file_to_zip(
filename,
interclass_tag.str_tagtable(),
path="details_semestres",
)
"""Synthèse des éléments du jury PE"""
self.synthese = self.synthetise_juryPE()
# Export des données => mode 1 seule feuille -> supprimé
pe_comp.pe_print("*** Export du jury de synthese")
filename = "synthese_jury_" + str(self.diplome) + ".xlsx"
with pd.ExcelWriter(filename, engine="openpyxl") as writer:
for onglet in self.synthese:
df = self.synthese[onglet]
df.to_excel(
writer, onglet, index=True, header=True
) # écriture dans l'onglet
# worksheet = writer.sheets[onglet] # l'on
self.add_file_to_zip(
filename,
open(filename, "rb").read(),
)
"""Fin !!!! Tada :)"""
def add_file_to_zip(self, filename: str, data, path=""):
"""Add a file to our zip
All files under NOM_EXPORT_ZIP/
path may specify a subdirectory
Args:
filename: Le nom du fichier à intégrer au zip
data: Les données du fichier
path: Un dossier dans l'arborescence du zip
"""
path_in_zip = os.path.join(path, filename) # self.nom_export_zip,
self.zipfile.writestr(path_in_zip, data)
def get_zipped_data(self):
"""returns file-like data with a zip of all generated (CSV) files.
Reset file cursor at the beginning !
"""
if self.zipfile:
self.zipfile.close()
self.zipfile = None
self.zipdata.seek(0)
return self.zipdata
def do_tags_list(self, interclassements: dict[str, AggregatInterclasseTag]):
"""La liste des tags extraites des interclassements"""
tags = []
for aggregat in interclassements:
interclass = interclassements[aggregat]
if interclass.tags_sorted:
tags.extend(interclass.tags_sorted)
tags = sorted(set(tags))
return tags
# **************************************************************************************************************** #
# Méthodes pour la synthèse du juryPE
# *****************************************************************************************************************
def synthetise_juryPE(self):
"""Synthétise tous les résultats du jury PE dans des dataframes"""
pe_comp.pe_print("*** Synthèse finale des moyennes ***")
synthese = {}
pe_comp.pe_print(" -> Synthèse des données administratives")
synthese["administratif"] = self.df_administratif()
tags = self.do_tags_list(self.interclassements_taggues)
for tag in tags:
pe_comp.pe_print(f" -> Synthèse du tag {tag}")
synthese[tag] = self.df_tag(tag)
return synthese
def df_administratif(self):
"""Synthétise toutes les données administratives des étudiants"""
etudids = list(self.diplomes_ids)
"""Récupération des données des étudiants"""
administratif = {}
nbre_semestres_max = self.etudiants.nbre_etapes_max_diplomes()
for etudid in etudids:
etudiant = self.etudiants.identites[etudid]
cursus = self.etudiants.cursus[etudid]
formsemestres = cursus["formsemestres"]
administratif[etudid] = {
"Nom": etudiant.nom,
"Prenom": etudiant.prenom,
"Civilite": etudiant.civilite_str,
"Age": pe_comp.calcul_age(etudiant.date_naissance),
"Date d'entree": cursus["entree"],
"Date de diplome": cursus["diplome"],
"Nbre de semestres": len(formsemestres),
}
# Ajout des noms de semestres parcourus
etapes = pe_affichage.etapes_du_cursus(formsemestres, nbre_semestres_max)
administratif[etudid] |= etapes
"""Construction du dataframe"""
df = pd.DataFrame.from_dict(administratif, orient="index")
"""Tri par nom/prénom"""
df.sort_values(by=["Nom", "Prenom"], inplace = True)
return df
def df_tag(self, tag):
"""Génère le DataFrame synthétisant les moyennes/classements (groupe,
interclassement promo) pour tous les aggrégats prévus,
tels que fourni dans l'excel final.
Args:
tag: Un des tags (a minima `but`)
Returns:
"""
etudids = list(self.diplomes_ids)
aggregats = pe_comp.TOUS_LES_PARCOURS
donnees = {}
for etudid in etudids:
etudiant = self.etudiants.identites[etudid]
donnees[etudid] = {
"Nom": etudiant.nom,
"Prenom": etudiant.prenom,
"Civilite": etudiant.civilite_str,
}
for aggregat in aggregats:
"""La trajectoire de l'étudiant sur l'aggrégat"""
trajectoire = self.trajectoires.suivi[etudid][aggregat]
"""Les moyennes par tag de cette trajectoire"""
if trajectoire:
trajectoire_tagguee = self.trajectoires_tagguees[
trajectoire.trajectoire_id
]
bilan = trajectoire_tagguee.moyennes_tags[tag]
donnees[etudid] |= {
f"{aggregat} notes ": f"{bilan['notes'].loc[etudid]:.1f}",
f"{aggregat} class. (groupe)": f"{bilan['classements'].loc[etudid]}/{bilan['nb_inscrits']}",
f"{aggregat} min/moy/max (groupe)": f"{bilan['min']:.1f}/{bilan['moy']:.1f}/{bilan['max']:.1f}",
}
else:
donnees[etudid] |= {
f"{aggregat} notes ": "-",
f"{aggregat} class. (groupe)": "-",
f"{aggregat} min/moy/max (groupe)": "-",
}
"""L'interclassement"""
interclass = self.interclassements_taggues[aggregat]
if tag in interclass.moyennes_tags:
bilan = interclass.moyennes_tags[tag]
donnees[etudid] |= {
f"{aggregat} class. (promo)": f"{bilan['classements'].loc[etudid]}/{bilan['nb_inscrits']}",
f"{aggregat} min/moy/max (promo)": f"{bilan['min']:.1f}/{bilan['moy']:.1f}/{bilan['max']:.1f}",
}
else:
donnees[etudid] |= {
f"{aggregat} class. (promo)": "-",
f"{aggregat} min/moy/max (promo)": "-",
}
# Fin de l'aggrégat
"""Construction du dataFrame"""
df = pd.DataFrame.from_dict(donnees, orient="index")
"""Tri par nom/prénom"""
df.sort_values(by=["Nom", "Prenom"], inplace = True)
return df
def table_syntheseJury(self, mode="singlesheet"): # was str_syntheseJury
"""Table(s) du jury
mode: singlesheet ou multiplesheet pour export excel
"""
sT = SeqGenTable() # le fichier excel à générer
if mode == "singlesheet":
return sT.get_genTable("singlesheet")
else:
return sT
def compute_semestres_tag(etudiants: EtudiantsJuryPE) -> dict:
"""Créé les semestres taggués, de type 'S1', 'S2', ..., pour un groupe d'étudiants donnés.
Chaque semestre taggué est rattaché à l'un des FormSemestre faisant partie du cursus scolaire
des étudiants (cf. attribut etudiants.cursus).
En crééant le semestre taggué, sont calculées les moyennes/classements par tag associé.
.
Args:
etudiants: Un groupe d'étudiants participant au jury
Returns:
Un dictionnaire {fid: SemestreTag(fid)}
"""
"""Création des semestres taggués, de type 'S1', 'S2', ..."""
pe_comp.pe_print("*** Création des semestres taggués")
formsemestres = etudiants.get_formsemestres(
semestres_recherches=pe_comp.TOUS_LES_SEMESTRES
)
semestres_tags = {}
for frmsem_id, formsemestre in formsemestres.items():
"""Choix d'un nom pour le semestretag"""
nom = "S%d %d %d-%d" % (
formsemestre.semestre_id,
frmsem_id,
formsemestre.date_debut.year,
formsemestre.date_fin.year,
)
pe_comp.pe_print(f" --> Semestre taggué {nom} sur la base de {formsemestre}")
"""Créé le semestre_tag et exécute les calculs de moyennes"""
formsemestretag = SemestreTag(nom, frmsem_id)
"""Stocke le semestre taggué"""
semestres_tags[frmsem_id] = formsemestretag
return semestres_tags
def compute_trajectoires_tag(
trajectoires: TrajectoiresJuryPE,
etudiants: EtudiantsJuryPE,
semestres_taggues: dict[int, SemestreTag],
):
"""Créée les trajectoires tagguées (combinaison aggrégeant plusieurs semestres au sens
d'un aggrégat (par ex: '3S')),
en calculant les moyennes et les classements par tag pour chacune.
Pour rappel : Chaque trajectoire est identifiée un nom d'aggrégat et par un formsemestre terminal.
Par exemple :
* combinaisons '3S' : S1+S2+S3 en prenant en compte tous les S3 qu'ont fréquenté les
étudiants du jury PE. Ces S3 marquent les formsemestre terminal de chaque combinaison.
* combinaisons 'S2' : 1 seul S2 pour des étudiants n'ayant pas redoublé, 2 pour des redoublants (dont les
notes seront moyennées sur leur 2 semestres S2). Ces combinaisons ont pour formsemestre le dernier S2 en
date (le S2 redoublé par les redoublants est forcément antérieur)
Args:
etudiants: Les données des étudiants
semestres_tag: Les semestres tag (pour lesquels des moyennes par tag ont été calculés)
Return:
Un dictionnaire de la forme {nom_aggregat: {fid_terminal: SetTag(fid_terminal)} }
"""
pe_comp.pe_print(" *** Création des aggrégats ")
trajectoires_tagguees = {}
for trajectoire_id in trajectoires.trajectoires:
trajectoire = trajectoires.trajectoires[trajectoire_id]
nom = trajectoire.get_repr()
pe_comp.pe_print(f" --> Fusion {nom}")
"""Création de la trajectoire_tagguee associée"""
trajectoire_tagguee = TrajectoireTag(
nom, trajectoire, semestres_taggues, etudiants
)
"""Mémorise le résultat"""
trajectoires_tagguees[trajectoire_id] = trajectoire_tagguee
return trajectoires_tagguees
def compute_interclassements(
etudiants: EtudiantsJuryPE,
trajectoires_jury_pe: TrajectoiresJuryPE,
trajectoires_tagguees: dict[tuple, Trajectoire],
):
"""Interclasse les étudiants, (nom d') aggrégat par aggrégat,
pour fournir un classement sur la promo. Le classement est établi au regard du nombre
d'étudiants ayant participé au même aggrégat.
"""
pe_comp.pe_print(" Interclassement sur la promo")
aggregats_interclasses_taggues = {}
for nom_aggregat in pe_comp.TOUS_LES_SEMESTRES + pe_comp.TOUS_LES_AGGREGATS:
pe_comp.pe_print(f" --> {nom_aggregat}")
interclass = AggregatInterclasseTag(
nom_aggregat, etudiants, trajectoires_jury_pe, trajectoires_tagguees
)
aggregats_interclasses_taggues[nom_aggregat] = interclass
return aggregats_interclasses_taggues

File diff suppressed because it is too large Load Diff

View File

@ -37,110 +37,96 @@ Created on Fri Sep 9 09:15:05 2016
"""
from app import db, log
from app.comp import res_sem
from app.comp import res_sem, moy_ue, moy_sem
from app.comp.res_compat import NotesTableCompat
from app.comp.res_sem import load_formsemestre_results
from app.models import FormSemestre
from app.models.moduleimpls import ModuleImpl
from app.pe import pe_tagtable
from app.scodoc import codes_cursus
from app.scodoc import sco_tag_module
from app.scodoc import sco_utils as scu
from app.scodoc.codes_cursus import UE_SPORT
import app.pe.pe_comp as pe_comp
from app.pe.pe_tabletags import (TableTag, TAGS_RESERVES)
class SemestreTag(pe_tagtable.TableTag):
"""Un SemestreTag représente un tableau de notes (basé sur notesTable)
modélisant les résultats des étudiants sous forme de moyennes par tag.
Attributs récupérés via des NotesTables :
- nt: le tableau de notes du semestre considéré
- nt.inscrlist: étudiants inscrits à ce semestre, par ordre alphabétique (avec demissions)
- nt.identdict: { etudid : ident }
- liste des moduleimpl { ... 'module_id', ...}
Attributs supplémentaires :
- inscrlist/identdict: étudiants inscrits hors démissionnaires ou défaillants
- _tagdict : Dictionnaire résumant les tags et les modules du semestre auxquels ils sont liés
Attributs hérités de TableTag :
- nom :
- resultats: {tag: { etudid: (note_moy, somme_coff), ...} , ...}
- rang
- statistiques
Redéfinition :
- get_etudids() : les etudids des étudiants non défaillants ni démissionnaires
class SemestreTag(TableTag):
"""Un SemestreTag représente les résultats des étudiants à un semestre, en donnant
accès aux moyennes par tag.
Il s'appuie principalement sur FormSemestre et sur ResultatsSemestreBUT.
"""
DEBUG = True
# -----------------------------------------------------------------------------
# Fonctions d'initialisation
# -----------------------------------------------------------------------------
def __init__(self, notetable, sem): # Initialisation sur la base d'une notetable
"""Instantiation d'un objet SemestreTag à partir d'un tableau de note
et des informations sur le semestre pour le dater
def __init__(self, nom: str, formsemestre_id: int):
"""
pe_tagtable.TableTag.__init__(
self,
nom="S%d %s %s-%s"
% (
sem["semestre_id"],
"ENEPS"
if "ENEPS" in sem["titre"]
else "UFA"
if "UFA" in sem["titre"]
else "FI",
sem["annee_debut"],
sem["annee_fin"],
),
)
Args:
nom: Nom à donner au SemestreTag
formsemestre_id: Identifiant du FormSemestre sur lequel il se base
"""
TableTag.__init__(self, nom=nom)
# Les attributs spécifiques
self.nt = notetable
"""Le semestre"""
self.formsemestre_id = formsemestre_id
self.formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
# Les attributs hérités : la liste des étudiants
self.inscrlist = [
etud
for etud in self.nt.inscrlist
if self.nt.get_etud_etat(etud["etudid"]) == scu.INSCRIT
]
self.identdict = {
etudid: ident
for (etudid, ident) in self.nt.identdict.items()
if etudid in self.get_etudids()
} # Liste des étudiants non démissionnaires et non défaillants
"""Les résultats du semestre"""
self.nt = load_formsemestre_results(self.formsemestre)
# Les modules pris en compte dans le calcul des moyennes par tag => ceux des UE standards
self.modimpls = [
modimpl
for modimpl in self.nt.formsemestre.modimpls_sorted
if modimpl.module.ue.type == codes_cursus.UE_STANDARD
] # la liste des modules (objet modimpl)
self.somme_coeffs = sum(
[
modimpl.module.coefficient
for modimpl in self.modimpls
if modimpl.module.coefficient is not None
]
)
"""Les étudiants"""
self.etuds = self.nt.etuds
self.etudiants = {etud.etudid: etud.etat_civil for etud in self.etuds}
# -----------------------------------------------------------------------------
def comp_data_semtag(self):
"""Calcule tous les données numériques associées au semtag"""
# Attributs relatifs aux tag pour les modules pris en compte
self.tagdict = (
self.do_tagdict()
) # Dictionnaire résumant les tags et les données (normalisées) des modules du semestre auxquels ils sont liés
"""Les notes, les modules implémentés triés, les étudiants, les coeffs,
récupérés notamment de py:mod:`res_but`
"""
self.sem_cube = self.nt.sem_cube
self.modimpls_sorted = self.nt.formsemestre.modimpls_sorted
self.modimpl_coefs_df = self.nt.modimpl_coefs_df
# Calcul des moyennes de chaque étudiant puis ajoute la moyenne au sens "DUT"
for tag in self.tagdict:
self.add_moyennesTag(tag, self.comp_MoyennesTag(tag, force=True))
self.add_moyennesTag("dut", self.get_moyennes_DUT())
self.taglist = sorted(
list(self.tagdict.keys()) + ["dut"]
) # actualise la liste des tags
"""Les inscriptions au module et les dispenses d'UE"""
self.modimpl_inscr_df = self.nt.modimpl_inscr_df
self.ues = self.nt.ues
self.ues_inscr_parcours_df = self.nt.load_ues_inscr_parcours()
self.dispense_ues = self.nt.dispense_ues
"""Les tags (en supprimant les tags réservés)"""
self.tags = get_synthese_tags_semestre(self.nt.formsemestre)
for tag in TAGS_RESERVES:
if tag in self.tags:
del self.tags[tag]
"""Calcul des moyennes & les classements de chaque étudiant à chaque tag"""
self.moyennes_tags = {}
for tag in self.tags:
pe_comp.pe_print(f" -> Traitement du tag {tag}")
moy_gen_tag = self.compute_moyenne_tag(tag)
class_gen_tag = moy_sem.comp_ranks_series(moy_gen_tag)[1] # en int
self.moyennes_tags[tag] = {
"notes": moy_gen_tag,
"classements": class_gen_tag,
"min": moy_gen_tag.min(),
"max": moy_gen_tag.max(),
"moy": moy_gen_tag.mean(),
"nb_inscrits": len(moy_gen_tag),
}
"""Ajoute les moyennes générales de BUT pour le semestre considéré"""
pe_comp.pe_print(f" -> Traitement du tag but")
moy_gen_but = self.nt.etud_moy_gen
class_gen_but = self.nt.etud_moy_gen_ranks_int
self.moyennes_tags["but"] = {
"notes": moy_gen_but,
"classements": class_gen_but,
"min": moy_gen_but.min(),
"max": moy_gen_but.max(),
"moy": moy_gen_but.mean(),
"nb_inscrits": len(moy_gen_but),
}
"""Synthétise l'ensemble des moyennes dans un dataframe"""
self.tags_sorted = sorted(self.moyennes_tags) # les tags par ordre alphabétique
self.notes = self.df_tagtable() # Le dataframe synthétique des notes (=moyennes par tag)
# -----------------------------------------------------------------------------
def get_etudids(self):
@ -148,89 +134,64 @@ class SemestreTag(pe_tagtable.TableTag):
return [etud["etudid"] for etud in self.inscrlist]
# -----------------------------------------------------------------------------
def do_tagdict(self):
"""Parcourt les modimpl du semestre (instance des modules d'un programme) et synthétise leurs données sous la
forme d'un dictionnaire reliant les tags saisis dans le programme aux
données des modules qui les concernent, à savoir les modimpl_id, les module_id, le code du module, le coeff,
la pondération fournie avec le tag (par défaut 1 si non indiquée).
{ tagname1 : { modimpl_id1 : { 'module_id' : ..., 'coeff' : ..., 'coeff_norm' : ..., 'ponderation' : ..., 'module_code' : ..., 'ue_xxx' : ...},
modimpl_id2 : ....
},
tagname2 : ...
}
Renvoie le dictionnaire ainsi construit.
def compute_moyenne_tag(self, tag: str) -> list:
"""Calcule la moyenne des étudiants pour le tag indiqué,
pour ce SemestreTag.
Rq: choix fait de repérer les modules par rapport à leur modimpl_id (valable uniquement pour un semestre), car
correspond à la majorité des calculs de moyennes pour les étudiants
(seuls ceux qui ont capitalisé des ue auront un régime de calcul différent).
"""
tagdict = {}
Sont pris en compte les modules implémentés associés au tag,
avec leur éventuel coefficient de **repondération**, en utilisant les notes
chargées pour ce SemestreTag.
for modimpl in self.modimpls:
modimpl_id = modimpl.id
# liste des tags pour le modimpl concerné:
tags = sco_tag_module.module_tag_list(modimpl.module.id)
for (
tag
) in tags: # tag de la forme "mathématiques", "théorie", "pe:0", "maths:2"
[tagname, ponderation] = sco_tag_module.split_tagname_coeff(
tag
) # extrait un tagname et un éventuel coefficient de pondération (par defaut: 1)
# tagname = tagname
if tagname not in tagdict: # Ajout d'une clé pour le tag
tagdict[tagname] = {}
# Ajout du modimpl au tagname considéré
tagdict[tagname][modimpl_id] = {
"module_id": modimpl.module.id, # les données sur le module
"coeff": modimpl.module.coefficient, # le coeff du module dans le semestre
"ponderation": ponderation, # la pondération demandée pour le tag sur le module
"module_code": modimpl.module.code, # le code qui doit se retrouver à l'identique dans des ue capitalisee
"ue_id": modimpl.module.ue.id, # les données sur l'ue
"ue_code": modimpl.module.ue.ue_code,
"ue_acronyme": modimpl.module.ue.acronyme,
}
return tagdict
# -----------------------------------------------------------------------------
def comp_MoyennesTag(self, tag, force=False) -> list:
"""Calcule et renvoie les "moyennes" de tous les étudiants du SemTag
(non défaillants) à un tag donné, en prenant en compte
tous les modimpl_id concerné par le tag, leur coeff et leur pondération.
Force ou non le calcul de la moyenne lorsque des notes sont manquantes.
Renvoie les informations sous la forme d'une liste
[ (moy, somme_coeff_normalise, etudid), ...]
"""
lesMoyennes = []
for etudid in self.get_etudids():
(
notes,
coeffs_norm,
ponderations,
) = self.get_listesNotesEtCoeffsTagEtudiant(
tag, etudid
) # les notes associées au tag
coeffs = comp_coeff_pond(
coeffs_norm, ponderations
) # les coeff pondérés par les tags
(moyenne, somme_coeffs) = pe_tagtable.moyenne_ponderee_terme_a_terme(
notes, coeffs, force=force
)
lesMoyennes += [
(moyenne, somme_coeffs, etudid)
] # Un tuple (pour classement résumant les données)
return lesMoyennes
# -----------------------------------------------------------------------------
def get_moyennes_DUT(self):
"""Lit les moyennes DUT du semestre pour tous les étudiants
et les renvoie au même format que comp_MoyennesTag"""
return [
(self.nt.etud_moy_gen[etudid], 1.0, etudid) for etudid in self.get_etudids()
"""Adaptation du mask de calcul des moyennes au tag visé"""
modimpls_mask = [
modimpl.module.ue.type != UE_SPORT
for modimpl in self.formsemestre.modimpls_sorted
]
"""Désactive tous les modules qui ne sont pas pris en compte pour ce tag"""
for i, modimpl in enumerate(self.formsemestre.modimpls_sorted):
if modimpl.moduleimpl_id not in self.tags[tag]:
modimpls_mask[i] = False
"""Applique la pondération des coefficients"""
modimpl_coefs_ponderes_df = self.modimpl_coefs_df.copy()
for modimpl_id in self.tags[tag]:
ponderation = self.tags[tag][modimpl_id]["ponderation"]
modimpl_coefs_ponderes_df[modimpl_id] *= ponderation
"""Calcule les moyennes pour le tag visé dans chaque UE (dataframe etudid x ues)"""
moyennes_ues_tag = moy_ue.compute_ue_moys_apc(
self.sem_cube,
self.etuds,
self.formsemestre.modimpls_sorted,
self.modimpl_inscr_df,
modimpl_coefs_ponderes_df,
modimpls_mask,
self.dispense_ues,
block=self.formsemestre.block_moyennes,
)
"""Les ects"""
ects = self.ues_inscr_parcours_df.fillna(0.0) * [
ue.ects for ue in self.ues if ue.type != UE_SPORT
]
"""Calcule la moyenne générale dans le semestre (pondérée par le ECTS)"""
moy_gen_tag = moy_sem.compute_sem_moys_apc_using_ects(
moyennes_ues_tag,
ects,
formation_id=self.formsemestre.formation_id,
skip_empty_ues=True,
)
return moy_gen_tag
# -----------------------------------------------------------------------------
def get_noteEtCoeff_modimpl(self, modimpl_id, etudid, profondeur=2):
"""Renvoie un couple donnant la note et le coeff normalisé d'un étudiant à un module d'id modimpl_id.
@ -319,27 +280,6 @@ class SemestreTag(pe_tagtable.TableTag):
return self.nt.validations.ue_capitalisees.loc[[etudid]].to_dict("records")
return []
# -----------------------------------------------------------------------------
def get_listesNotesEtCoeffsTagEtudiant(self, tag, etudid):
"""Renvoie un triplet (notes, coeffs_norm, ponderations) où notes, coeff_norm et ponderation désignent trois listes
donnant -pour un tag donné- les note, coeff et ponderation de chaque modimpl à prendre en compte dans
le calcul de la moyenne du tag.
Les notes et coeff_norm sont extraits grâce à SemestreTag.get_noteEtCoeff_modimpl (donc dans semestre courant ou UE capitalisée).
Les pondérations sont celles déclarées avec le tag (cf. _tagdict)."""
notes = []
coeffs_norm = []
ponderations = []
for moduleimpl_id, modimpl in self.tagdict[
tag
].items(): # pour chaque module du semestre relatif au tag
(note, coeff_norm) = self.get_noteEtCoeff_modimpl(moduleimpl_id, etudid)
if note != None:
notes.append(note)
coeffs_norm.append(coeff_norm)
ponderations.append(modimpl["ponderation"])
return (notes, coeffs_norm, ponderations)
# -----------------------------------------------------------------------------
# Fonctions d'affichage (et d'export csv) des données du semestre en mode debug
# -----------------------------------------------------------------------------
@ -435,8 +375,9 @@ class SemestreTag(pe_tagtable.TableTag):
return chaine
def str_tagsModulesEtCoeffs(self):
"""Renvoie une chaine affichant la liste des tags associés au semestre, les modules qui les concernent et les coeffs de pondération.
Plus concrêtement permet d'afficher le contenu de self._tagdict"""
"""Renvoie une chaine affichant la liste des tags associés au semestre,
les modules qui les concernent et les coeffs de pondération.
Plus concrètement permet d'afficher le contenu de self._tagdict"""
chaine = "Semestre %s d'id %d" % (self.nom, id(self)) + "\n"
chaine += " -> somme de coeffs: " + str(self.somme_coeffs) + "\n"
taglist = self.get_all_tags()
@ -463,25 +404,6 @@ class SemestreTag(pe_tagtable.TableTag):
# *********************************************
def comp_coeff_pond(coeffs, ponderations):
"""
Applique une ponderation (indiquée dans la liste ponderations) à une liste de coefficients :
ex: coeff = [2, 3, 1, None], ponderation = [1, 2, 0, 1] => [2*1, 3*2, 1*0, None]
Les coeff peuvent éventuellement être None auquel cas None est conservé ;
Les pondérations sont des floattants
"""
if (
coeffs == None
or ponderations == None
or not isinstance(coeffs, list)
or not isinstance(ponderations, list)
or len(coeffs) != len(ponderations)
):
raise ValueError("Erreur de paramètres dans comp_coeff_pond")
return [
(None if coeffs[i] == None else coeffs[i] * ponderations[i])
for i in range(len(coeffs))
]
# -----------------------------------------------------------------------------
@ -509,3 +431,58 @@ def get_moy_ue_from_nt(nt, etudid, modimpl_id) -> float:
if ue_status is None:
return None
return ue_status["moy"]
# -----------------------------------------------------------------------------
def get_synthese_tags_semestre(formsemestre: FormSemestre):
"""Etant données les implémentations des modules du semestre (modimpls),
synthétise les tags les concernant (tags saisis dans le programme pédagogique)
en les associant aux modimpls qui les concernent (modimpl_id, module_id,
le code du module, coeff et pondération fournie avec le tag (par défaut 1 si non indiquée)).
{ tagname1: { modimpl_id1: { 'module_id': ...,
'coeff': ...,
'coeff_norm': ...,
'ponderation': ...,
'module_code': ...,
'ue_xxx': ...},
}
}
Args:
formsemestre: Le formsemestre à la base de la recherche des tags
"""
synthese_tags = {}
"""Instance des modules du semestre"""
modimpls = formsemestre.modimpls_sorted
for modimpl in modimpls:
modimpl_id = modimpl.id
"""Liste des tags pour le module concerné"""
tags = sco_tag_module.module_tag_list(modimpl.module.id)
"""Traitement des tags recensés, chacun pouvant étant de la forme
"mathématiques", "théorie", "pe:0", "maths:2"
"""
for tag in tags:
"""Extraction du nom du tag et du coeff de pondération"""
(tagname, ponderation) = sco_tag_module.split_tagname_coeff(tag)
"""Ajout d'une clé pour le tag"""
if tagname not in synthese_tags:
synthese_tags[tagname] = {}
"""Ajout du module (modimpl) au tagname considéré"""
synthese_tags[tagname][modimpl_id] = {
"modimpl": modimpl, # les données sur le module
# "coeff": modimpl.module.coefficient, # le coeff du module dans le semestre
"ponderation": ponderation, # la pondération demandée pour le tag sur le module
# "module_code": modimpl.module.code, # le code qui doit se retrouver à l'identique dans des ue capitalisee
# "ue_id": modimpl.module.ue.id, # les données sur l'ue
# "ue_code": modimpl.module.ue.ue_code,
# "ue_acronyme": modimpl.module.ue.acronyme,
}
return synthese_tags

View File

@ -1,324 +0,0 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
##############################################################################
# Module "Avis de poursuite d'étude"
# conçu et développé par Cléo Baras (IUT de Grenoble)
##############################################################################
"""
Created on Fri Sep 9 09:15:05 2016
@author: barasc
"""
from app.pe.pe_tools import pe_print, PE_DEBUG
from app.pe import pe_tagtable
class SetTag(pe_tagtable.TableTag):
"""Agrège plusieurs semestres (ou settag) taggués (SemestreTag/Settag de 1 à 4) pour extraire des moyennes
et des classements par tag pour un groupe d'étudiants donnés.
par. exemple fusion d'un parcours ['S1', 'S2', 'S3'] donnant un nom_combinaison = '3S'
Le settag est identifié sur la base du dernier semestre (ici le 'S3') ;
les étudiants considérés sont donc ceux inscrits dans ce S3
à condition qu'ils disposent d'un parcours sur tous les semestres fusionnés valides (par. ex
un etudiant non inscrit dans un S1 mais dans un S2 et un S3 n'est pas pris en compte).
"""
# -------------------------------------------------------------------------------------------------------------------
def __init__(self, nom_combinaison, parcours):
pe_tagtable.TableTag.__init__(self, nom=nom_combinaison)
self.combinaison = nom_combinaison
self.parcours = parcours # Le groupe de semestres/parcours à aggréger
# -------------------------------------------------------------------------------------------
def set_Etudiants(
self, etudiants: list[dict], juryPEDict, etudInfoDict, nom_sem_final=None
):
"""Détermine la liste des étudiants à prendre en compte, en partant de
la liste en paramètre et en vérifiant qu'ils ont tous un parcours valide."""
if nom_sem_final:
self.nom += "_" + nom_sem_final
for etudid in etudiants:
parcours_incomplet = (
sum([juryPEDict[etudid][nom_sem] is None for nom_sem in self.parcours])
> 0
) # manque-t-il des formsemestre_id validant aka l'étudiant n'a pas été inscrit dans tous les semestres de l'aggrégat
if not parcours_incomplet:
self.inscrlist.append(etudInfoDict[etudid])
self.identdict[etudid] = etudInfoDict[etudid]
delta = len(etudiants) - len(self.inscrlist)
if delta > 0:
pe_print(self.nom + " -> " + str(delta) + " étudiants supprimés")
# Le sous-ensemble des parcours
self.parcoursDict = {etudid: juryPEDict[etudid] for etudid in self.identdict}
# -------------------------------------------------------------------------------------------
def get_Fids_in_settag(self):
"""Renvoie la liste des semestres (leur formsemestre_id) à prendre en compte
pour le calcul des moyennes, en considérant tous les étudiants inscrits et
tous les semestres de leur parcours"""
return list(
{
self.parcoursDict[etudid][nom_sem]
for etudid in self.identdict
for nom_sem in self.parcours
}
)
# ---------------------------------------------------------------------------------------------
def set_SemTagDict(self, SemTagDict):
"""Mémorise les semtag nécessaires au jury."""
self.SemTagDict = {fid: SemTagDict[fid] for fid in self.get_Fids_in_settag()}
if PE_DEBUG >= 1:
pe_print(" => %d semestres fusionnés" % len(self.SemTagDict))
# -------------------------------------------------------------------------------------------------------------------
def comp_data_settag(self):
"""Calcule tous les données numériques relatives au settag"""
# Attributs relatifs aux tag pour les modules pris en compte
self.taglist = self.do_taglist() # la liste des tags
self.do_tagdict() # le dico descriptif des tags
# if PE_DEBUG >= 1: pe_print(" => Tags = " + ", ".join( self.taglist ))
# Calcul des moyennes de chaque étudiant par tag
reussiteAjoutTag = {"OK": [], "KO": []}
for tag in self.taglist:
moyennes = self.comp_MoyennesSetTag(tag, force=False)
res = self.add_moyennesTag(tag, moyennes) # pas de notes => pas de moyenne
reussiteAjoutTag["OK" if res else "KO"].append(tag)
if len(reussiteAjoutTag["OK"]) > 0 and PE_DEBUG:
pe_print(
" => Fusion de %d tags : " % (len(reussiteAjoutTag["OK"]))
+ ", ".join(reussiteAjoutTag["OK"])
)
if len(reussiteAjoutTag["KO"]) > 0 and PE_DEBUG:
pe_print(
" => %d tags manquants : " % (len(reussiteAjoutTag["KO"]))
+ ", ".join(reussiteAjoutTag["KO"])
)
# -------------------------------------------------------------------------------------------------------------------
def get_etudids(self):
return list(self.identdict.keys())
# -------------------------------------------------------------------------------------------------------------------
def do_taglist(self):
"""Parcourt les tags des semestres taggués et les synthétise sous la forme
d'une liste en supprimant les doublons
"""
ensemble = []
for semtag in self.SemTagDict.values():
ensemble.extend(semtag.get_all_tags())
return sorted(list(set(ensemble)))
# -------------------------------------------------------------------------------------------------------------------
def do_tagdict(self):
"""Synthétise la liste des modules pris en compte dans le calcul d'un tag (pour analyse des résultats)"""
self.tagdict = {}
for semtag in self.SemTagDict.values():
for tag in semtag.get_all_tags():
if tag != "dut":
if tag not in self.tagdict:
self.tagdict[tag] = {}
for mod in semtag.tagdict[tag]:
self.tagdict[tag][mod] = semtag.tagdict[tag][mod]
# -------------------------------------------------------------------------------------------------------------------
def get_NotesEtCoeffsSetTagEtudiant(self, tag, etudid):
"""Récupère tous les notes et les coeffs d'un étudiant relatives à un tag dans ses semestres valides et les renvoie dans un tuple (notes, coeffs)
avec notes et coeffs deux listes"""
lesSemsDeLEtudiant = [
self.parcoursDict[etudid][nom_sem] for nom_sem in self.parcours
] # peuvent être None
notes = [
self.SemTagDict[fid].get_moy_from_resultats(tag, etudid)
for fid in lesSemsDeLEtudiant
if tag in self.SemTagDict[fid].taglist
] # eventuellement None
coeffs = [
self.SemTagDict[fid].get_coeff_from_resultats(tag, etudid)
for fid in lesSemsDeLEtudiant
if tag in self.SemTagDict[fid].taglist
]
return (notes, coeffs)
# -------------------------------------------------------------------------------------------------------------------
def comp_MoyennesSetTag(self, tag, force=False):
"""Calcule et renvoie les "moyennes" des étudiants à un tag donné, en prenant en compte tous les semestres taggués
de l'aggrégat, et leur coeff Par moyenne, s'entend une note moyenne, la somme des coefficients de pondération
appliqué dans cette moyenne.
Force ou non le calcul de la moyenne lorsque des notes sont manquantes.
Renvoie les informations sous la forme d'une liste [etudid: (moy, somme_coeff_normalisée, rang), ...}
"""
# if tag not in self.get_all_tags() : return None
# Calcule les moyennes
lesMoyennes = []
for (
etudid
) in (
self.get_etudids()
): # Pour tous les étudiants non défaillants du semestre inscrits dans des modules relatifs au tag
(notes, coeffs_norm) = self.get_NotesEtCoeffsSetTagEtudiant(
tag, etudid
) # lecture des notes associées au tag
(moyenne, somme_coeffs) = pe_tagtable.moyenne_ponderee_terme_a_terme(
notes, coeffs_norm, force=force
)
lesMoyennes += [
(moyenne, somme_coeffs, etudid)
] # Un tuple (pour classement résumant les données)
return lesMoyennes
class SetTagInterClasse(pe_tagtable.TableTag):
"""Récupère les moyennes de SetTag aggrégant un même parcours (par ex un ['S1', 'S2'] n'ayant pas fini au même S2
pour fournir un interclassement sur un groupe d'étudiant => seul compte alors la promo
nom_combinaison = 'S1' ou '1A'
"""
# -------------------------------------------------------------------------------------------------------------------
def __init__(self, nom_combinaison, diplome):
pe_tagtable.TableTag.__init__(self, nom=f"{nom_combinaison}_{diplome or ''}")
self.combinaison = nom_combinaison
self.parcoursDict = {}
# -------------------------------------------------------------------------------------------
def set_Etudiants(self, etudiants, juryPEDict, etudInfoDict, nom_sem_final=None):
"""Détermine la liste des étudiants à prendre en compte, en partant de
la liste fournie en paramètre et en vérifiant que l'étudiant dispose bien d'un parcours valide pour la combinaison demandée.
Renvoie le nombre d'étudiants effectivement inscrits."""
if nom_sem_final:
self.nom += "_" + nom_sem_final
for etudid in etudiants:
if juryPEDict[etudid][self.combinaison] != None:
self.inscrlist.append(etudInfoDict[etudid])
self.identdict[etudid] = etudInfoDict[etudid]
self.parcoursDict[etudid] = juryPEDict[etudid]
return len(self.inscrlist)
# -------------------------------------------------------------------------------------------
def get_Fids_in_settag(self):
"""Renvoie la liste des semestres (les formsemestre_id finissant la combinaison par ex. '3S' dont les fid des S3) à prendre en compte
pour les moyennes, en considérant tous les étudiants inscrits"""
return list(
{self.parcoursDict[etudid][self.combinaison] for etudid in self.identdict}
)
# ---------------------------------------------------------------------------------------------
def set_SetTagDict(self, SetTagDict):
"""Mémorise les settag nécessaires au jury."""
self.SetTagDict = {
fid: SetTagDict[fid] for fid in self.get_Fids_in_settag() if fid != None
}
if PE_DEBUG >= 1:
pe_print(" => %d semestres utilisés" % len(self.SetTagDict))
# -------------------------------------------------------------------------------------------------------------------
def comp_data_settag(self):
"""Calcule tous les données numériques relatives au settag"""
# Attributs relatifs aux tag pour les modules pris en compte
self.taglist = self.do_taglist()
# if PE_DEBUG >= 1: pe_print(" => Tags = " + ", ".join( self.taglist ))
# Calcul des moyennes de chaque étudiant par tag
reussiteAjoutTag = {"OK": [], "KO": []}
for tag in self.taglist:
moyennes = self.get_MoyennesSetTag(tag, force=False)
res = self.add_moyennesTag(tag, moyennes) # pas de notes => pas de moyenne
reussiteAjoutTag["OK" if res else "KO"].append(tag)
if len(reussiteAjoutTag["OK"]) > 0 and PE_DEBUG:
pe_print(
" => Interclassement de %d tags : " % (len(reussiteAjoutTag["OK"]))
+ ", ".join(reussiteAjoutTag["OK"])
)
if len(reussiteAjoutTag["KO"]) > 0 and PE_DEBUG:
pe_print(
" => %d tags manquants : " % (len(reussiteAjoutTag["KO"]))
+ ", ".join(reussiteAjoutTag["KO"])
)
# -------------------------------------------------------------------------------------------------------------------
def get_etudids(self):
return list(self.identdict.keys())
# -------------------------------------------------------------------------------------------------------------------
def do_taglist(self):
"""Parcourt les tags des semestres taggués et les synthétise sous la forme
d'une liste en supprimant les doublons
"""
ensemble = []
for settag in self.SetTagDict.values():
ensemble.extend(settag.get_all_tags())
return sorted(list(set(ensemble)))
# -------------------------------------------------------------------------------------------------------------------
def get_NotesEtCoeffsSetTagEtudiant(self, tag, etudid):
"""Récupère tous les notes et les coeffs d'un étudiant relatives à un tag dans ses semestres valides et les renvoie dans un tuple (notes, coeffs)
avec notes et coeffs deux listes"""
leSetTagDeLetudiant = self.parcoursDict[etudid][self.combinaison]
note = self.SetTagDict[leSetTagDeLetudiant].get_moy_from_resultats(tag, etudid)
coeff = self.SetTagDict[leSetTagDeLetudiant].get_coeff_from_resultats(
tag, etudid
)
return (note, coeff)
# -------------------------------------------------------------------------------------------------------------------
def get_MoyennesSetTag(self, tag, force=False):
"""Renvoie les "moyennes" des étudiants à un tag donné, en prenant en compte tous les settag de l'aggrégat,
et leur coeff Par moyenne, s'entend une note moyenne, la somme des coefficients de pondération
appliqué dans cette moyenne.
Force ou non le calcul de la moyenne lorsque des notes sont manquantes.
Renvoie les informations sous la forme d'une liste [etudid: (moy, somme_coeff_normalisée, rang), ...}
"""
# if tag not in self.get_all_tags() : return None
# Calcule les moyennes
lesMoyennes = []
for (
etudid
) in (
self.get_etudids()
): # Pour tous les étudiants non défaillants du semestre inscrits dans des modules relatifs au tag
(moyenne, somme_coeffs) = self.get_NotesEtCoeffsSetTagEtudiant(
tag, etudid
) # lecture des notes associées au tag
lesMoyennes += [
(moyenne, somme_coeffs, etudid)
] # Un tuple (pour classement résumant les données)
return lesMoyennes

View File

@ -41,36 +41,55 @@ import datetime
import numpy as np
from app.scodoc import sco_utils as scu
import pandas as pd
TAGS_RESERVES = ["but"]
class TableTag(object):
"""
Classe mémorisant les moyennes des étudiants à différents tag et permettant de calculer les rangs et les statistiques :
- nom : Nom représentatif des données de la Table
- inscrlist : Les étudiants inscrits dans le TagTag avec leur information de la forme :
Classe mémorisant les moyennes des étudiants à différents tags et permettant de
calculer des rangs et des statistiques.
Ses attributs sont:
* nom : Nom représentatif des données de la Table
* inscrlist : Les étudiants inscrits dans le TagTag avec leur information de la forme :
{ etudid : dictionnaire d'info extrait de Scodoc, ...}
- taglist : Liste triée des noms des tags
- resultats : Dictionnaire donnant les notes-moyennes de chaque étudiant par tag et la somme commulée
* taglist : Liste triée des noms des tags
* resultats : Dictionnaire donnant les notes-moyennes de chaque étudiant par tag et la somme commulée
des coeff utilisées dans le calcul de la moyenne pondérée, sous la forme :
{ tag : { etudid: (note_moy, somme_coeff_norm),
...}
- rangs : Dictionnaire donnant les rang par tag de chaque étudiant de la forme :
* rangs : Dictionnaire donnant les rang par tag de chaque étudiant de la forme :
{ tag : {etudid: rang, ...} }
- nbinscrits : Nombre d'inscrits dans le semestre (pas de distinction entre les tags)
- statistiques : Dictionnaire donnant les stastitiques (moyenne, min, max) des résultats par tag de la forme :
* nbinscrits : Nombre d'inscrits dans le semestre (pas de distinction entre les tags)
* statistiques : Dictionnaire donnant les statistiques (moyenne, min, max) des résultats par tag de la forme :
{ tag : (moy, min, max), ...}
"""
def __init__(self, nom=""):
def __init__(self, nom: str):
"""Les attributs basiques des TagTable, qui seront initialisés
dans les classes dérivées
"""
self.nom = nom
self.inscrlist = []
self.identdict = {}
self.taglist = []
"""Les étudiants"""
self.etudiants = {}
"""Les moyennes par tag"""
self.moyennes_tags = {}
# -----------------------------------------------------------------------------------------------------------
def get_all_tags(self):
"""Liste des tags de la table, triée par ordre alphabétique
Returns:
Liste de tags triés par ordre alphabétique
"""
return sorted(self.moyennes_tags.keys())
self.resultats = {}
self.rangs = {}
self.statistiques = {}
# *****************************************************************************************************************
# Accesseurs
@ -80,8 +99,8 @@ class TableTag(object):
def get_moy_from_resultats(self, tag, etudid):
"""Renvoie la moyenne obtenue par un étudiant à un tag donné au regard du format de self.resultats"""
return (
self.resultats[tag][etudid][0]
if tag in self.resultats and etudid in self.resultats[tag]
self.moyennes_tags[tag][etudid][0]
if tag in self.moyennes_tags and etudid in self.moyennes_tags[tag]
else None
)
@ -90,7 +109,7 @@ class TableTag(object):
"""Renvoie le rang à un tag d'un étudiant au regard du format de self.resultats"""
return (
self.rangs[tag][etudid]
if tag in self.resultats and etudid in self.resultats[tag]
if tag in self.moyennes_tags and etudid in self.moyennes_tags[tag]
else None
)
@ -100,16 +119,11 @@ class TableTag(object):
au regard du format de self.resultats.
"""
return (
self.resultats[tag][etudid][1]
if tag in self.resultats and etudid in self.resultats[tag]
self.moyennes_tags[tag][etudid][1]
if tag in self.moyennes_tags and etudid in self.moyennes_tags[tag]
else None
)
# -----------------------------------------------------------------------------------------------------------
def get_all_tags(self):
"""Renvoie la liste des tags du semestre triée par ordre alphabétique"""
# return self.taglist
return sorted(self.resultats.keys())
# -----------------------------------------------------------------------------------------------------------
def get_nbinscrits(self):
@ -170,10 +184,12 @@ class TableTag(object):
avec calcul du rang
:param tag: Un tag
:param listMoyEtCoeff: Une liste donnant [ (moy, coeff, etudid) ]
TODO:: Inutile maintenant ?
"""
# ajout des moyennes au dictionnaire résultat
if listMoyEtCoeff:
self.resultats[tag] = {
self.moyennes_tags[tag] = {
etudid: (moyenne, somme_coeffs)
for (moyenne, somme_coeffs, etudid) in listMoyEtCoeff
}
@ -204,11 +220,12 @@ class TableTag(object):
self.statistiques
"""
stats = ("-NA-", "-", "-")
if tag not in self.resultats:
if tag not in self.moyennes_tags:
return stats
notes = [
self.get_moy_from_resultats(tag, etudid) for etudid in self.resultats[tag]
self.get_moy_from_resultats(tag, etudid)
for etudid in self.moyennes_tags[tag]
] # les notes du tag
notes_valides = [
note for note in notes if isinstance(note, float) and note != None
@ -225,7 +242,7 @@ class TableTag(object):
"""Renvoie une chaine de caractères (valable pour un csv)
décrivant la moyenne et le rang d'un étudiant, pour un tag donné ;
"""
if tag not in self.get_all_tags() or etudid not in self.resultats[tag]:
if tag not in self.get_all_tags() or etudid not in self.moyennes_tags[tag]:
return ""
moystr = TableTag.str_moytag(
@ -256,30 +273,32 @@ class TableTag(object):
str_moytag = classmethod(str_moytag)
# -----------------------------------------------------------------------
def str_tagtable(self, delim=";", decimal_sep=","):
"""Renvoie une chaine de caractère listant toutes les moyennes, les rangs des étudiants pour tous les tags."""
entete = ["etudid", "nom", "prenom"]
for tag in self.get_all_tags():
entete += [titre + "_" + tag for titre in ["note", "rang", "nb_inscrit"]]
chaine = delim.join(entete) + "\n"
def df_tagtable(self):
"""Renvoie un dataframe (etudid x tag) listant toutes les moyennes par tags
for etudid in self.identdict:
descr = delim.join(
[
etudid,
self.identdict[etudid]["nom"],
self.identdict[etudid]["prenom"],
]
)
descr += delim + self.str_res_d_un_etudiant(etudid, delim)
chaine += descr + "\n"
# Ajout des stats ... à faire
if decimal_sep != ".":
return chaine.replace(".", decimal_sep)
Returns:
Un dataframe etudids x tag (avec tag par ordre alphabétique)
"""
tags = self.get_all_tags()
if tags:
dict_series = {tag: self.moyennes_tags[tag]["notes"] for tag in tags}
df = pd.DataFrame(dict_series)
return df
else:
return chaine
return None
def str_tagtable(self):
"""Renvoie une chaine de caractère listant toutes les moyennes,
les rangs des étudiants pour tous les tags."""
etudiants = self.etudiants
df = pd.DataFrame.from_dict(etudiants, orient="index", columns=["nom"])
for tag in self.get_all_tags():
df = df.join(self.moyennes_tags[tag]["notes"].rename(f"moy {tag}"))
df = df.join(self.moyennes_tags[tag]["classements"].rename(f"class {tag}"))
return df.to_csv(sep=";")
# ************************************************************************

View File

@ -1,960 +0,0 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
##############################################################################
# Module "Avis de poursuite d'étude"
# conçu et développé par Cléo Baras (IUT de Grenoble)
##############################################################################
"""
Created on Thu Sep 8 09:36:33 2016
@author: barasc
"""
import os
import datetime
import re
import unicodedata
from flask import g
import app.scodoc.sco_utils as scu
from app import log
from app.scodoc.sco_logos import find_logo
PE_DEBUG = 0
if not PE_DEBUG:
# log to notes.log
def pe_print(*a, **kw):
# kw is ignored. log always add a newline
log(" ".join(a))
else:
pe_print = print # print function
# Generated LaTeX files are encoded as:
PE_LATEX_ENCODING = "utf-8"
# /opt/scodoc/tools/doc_poursuites_etudes
REP_DEFAULT_AVIS = os.path.join(scu.SCO_TOOLS_DIR, "doc_poursuites_etudes/")
REP_LOCAL_AVIS = os.path.join(scu.SCODOC_CFG_DIR, "doc_poursuites_etudes/")
PE_DEFAULT_AVIS_LATEX_TMPL = REP_DEFAULT_AVIS + "distrib/modeles/un_avis.tex"
PE_LOCAL_AVIS_LATEX_TMPL = REP_LOCAL_AVIS + "local/modeles/un_avis.tex"
PE_DEFAULT_FOOTER_TMPL = REP_DEFAULT_AVIS + "distrib/modeles/un_footer.tex"
PE_LOCAL_FOOTER_TMPL = REP_LOCAL_AVIS + "local/modeles/un_footer.tex"
# ----------------------------------------------------------------------------------------
# ----------------------------------------------------------------------------------------
def print_semestres_description(sems, avec_affichage_debug=False):
"""Dediee a l'affichage d'un semestre pour debug du module"""
def chaine_semestre(sem):
desc = (
"S"
+ str(sem["semestre_id"])
+ " "
+ sem["modalite"]
+ " "
+ sem["anneescolaire"]
)
desc += " (" + sem["annee_debut"] + "/" + sem["annee_fin"] + ") "
desc += str(sem["formation_id"]) + " / " + str(sem["formsemestre_id"])
desc += " - " + sem["titre_num"]
return desc
if avec_affichage_debug == True:
if isinstance(sems, list):
for sem in sems:
pe_print(chaine_semestre(sem))
else:
pe_print(chaine_semestre(sems))
# ----------------------------------------------------------------------------------------
def calcul_age(born):
"""Calcule l'age à partir de la date de naissance sous forme d'une chaine de caractère 'jj/mm/aaaa'.
Aucun test de validité sur le format de la date n'est fait.
"""
if not isinstance(born, str) or born == "":
return ""
donnees = born.split("/")
naissance = datetime.datetime(int(donnees[2]), int(donnees[1]), int(donnees[0]))
today = datetime.date.today()
return (
today.year
- naissance.year
- ((today.month, today.day) < (naissance.month, naissance.day))
)
# ----------------------------------------------------------------------------------------
def remove_accents(input_unicode_str):
"""Supprime les accents d'une chaine unicode"""
nfkd_form = unicodedata.normalize("NFKD", input_unicode_str)
only_ascii = nfkd_form.encode("ASCII", "ignore")
return only_ascii
def escape_for_latex(s):
"""Protège les caractères pour inclusion dans du source LaTeX"""
if not s:
return ""
conv = {
"&": r"\&",
"%": r"\%",
"$": r"\$",
"#": r"\#",
"_": r"\_",
"{": r"\{",
"}": r"\}",
"~": r"\textasciitilde{}",
"^": r"\^{}",
"\\": r"\textbackslash{}",
"<": r"\textless ",
">": r"\textgreater ",
}
exp = re.compile(
"|".join(
re.escape(key)
for key in sorted(list(conv.keys()), key=lambda item: -len(item))
)
)
return exp.sub(lambda match: conv[match.group()], s)
# ----------------------------------------------------------------------------------------
def list_directory_filenames(path):
"""List of regular filenames in a directory (recursive)
Excludes files and directories begining with .
"""
R = []
for root, dirs, files in os.walk(path, topdown=True):
dirs[:] = [d for d in dirs if d[0] != "."]
R += [os.path.join(root, fn) for fn in files if fn[0] != "."]
return R
def add_local_file_to_zip(zipfile, ziproot, pathname, path_in_zip):
"""Read pathname server file and add content to zip under path_in_zip"""
rooted_path_in_zip = os.path.join(ziproot, path_in_zip)
zipfile.write(filename=pathname, arcname=rooted_path_in_zip)
# data = open(pathname).read()
# zipfile.writestr(rooted_path_in_zip, data)
def add_refs_to_register(register, directory):
"""Ajoute les fichiers trouvés dans directory au registre (dictionaire) sous la forme
filename => pathname
"""
length = len(directory)
for pathname in list_directory_filenames(directory):
filename = pathname[length + 1 :]
register[filename] = pathname
def add_pe_stuff_to_zip(zipfile, ziproot):
"""Add auxiliary files to (already opened) zip
Put all local files found under config/doc_poursuites_etudes/local
and config/doc_poursuites_etudes/distrib
If a file is present in both subtrees, take the one in local.
Also copy logos
"""
register = {}
# first add standard (distrib references)
distrib_dir = os.path.join(REP_DEFAULT_AVIS, "distrib")
add_refs_to_register(register=register, directory=distrib_dir)
# then add local references (some oh them may overwrite distrib refs)
local_dir = os.path.join(REP_LOCAL_AVIS, "local")
add_refs_to_register(register=register, directory=local_dir)
# at this point register contains all refs (filename, pathname) to be saved
for filename, pathname in register.items():
add_local_file_to_zip(zipfile, ziproot, pathname, "avis/" + filename)
# Logos: (add to logos/ directory in zip)
logos_names = ["header", "footer"]
for name in logos_names:
logo = find_logo(logoname=name, dept_id=g.scodoc_dept_id)
if logo is not None:
add_local_file_to_zip(
zipfile, ziproot, logo.filepath, "avis/logos/" + logo.filename
)
# ----------------------------------------------------------------------------------------
# Variable pour le debug des avislatex (en squeezant le calcul du jury souvent long)
JURY_SYNTHESE_POUR_DEBUG = {
"EID1810": {
"nom": "ROUX",
"entree": "2016",
"civilite_str": "M.",
"promo": 2016,
"S2": {
"groupe": {
"informatique": (
13.184230769230767,
0.21666666666666667,
"18",
78,
9.731491508491509,
18.46846153846154,
18.46846153846154,
),
"technique": (
12.975409073359078,
0.6166666666666666,
"16",
78,
9.948540264387688,
18.29285714285714,
18.29285714285714,
),
"pe": (
12.016584900684544,
1.116666666666667,
"20",
78,
9.83147528118408,
17.691755169172936,
17.691755169172936,
),
"mathematiques": (
12.25,
0.1,
"15 ex",
78,
8.45153073717949,
19.0625,
19.0625,
),
"dut": (
12.43750128724589,
1.0,
"19",
78,
10.151630181286441,
17.881104750512645,
17.881104750512645,
),
},
"promo": {
"informatique": (
13.184230769230767,
0.21666666666666667,
"25",
73,
11.696187214611871,
18.51346153846154,
18.51346153846154,
),
"technique": (
12.975409073359078,
0.6166666666666666,
"23",
73,
11.862307379173147,
17.616047267953675,
17.616047267953675,
),
"pe": (
12.016584900684544,
1.116666666666667,
"28",
73,
11.571004424603757,
16.706338951857248,
16.706338951857248,
),
"mathematiques": (
12.25,
0.1,
"18 ex",
73,
10.00886454908676,
19.0625,
19.0625,
),
"dut": (
12.43750128724589,
1.0,
"25",
73,
11.88798432763965,
17.397627309377608,
17.397627309377608,
),
},
},
"S1": {
"groupe": {
"informatique": (
16.064999999999998,
0.16666666666666669,
"11",
82,
11.020296296296294,
19.325999999999997,
19.325999999999997,
),
"technique": (
14.513007894736845,
0.6333333333333333,
"11",
82,
11.195082967479676,
18.309764912280702,
18.309764912280702,
),
"pe": (
13.260301515151516,
1.1,
"19",
82,
10.976036277232245,
17.7460505050505,
17.7460505050505,
),
"mathematiques": (
11.142850000000001,
0.13333333333333333,
"34",
82,
10.314605121951217,
19.75,
19.75,
),
"dut": (
13.54367375,
1.0,
"19",
82,
11.22193801880508,
18.226902529333334,
18.226902529333334,
),
},
"promo": {
"informatique": (
16.064999999999998,
0.16666666666666669,
"15",
73,
13.265276712328768,
19.325999999999997,
19.325999999999997,
),
"technique": (
14.513007894736845,
0.6333333333333333,
"16",
73,
12.996048795361693,
18.309764912280702,
18.309764912280702,
),
"pe": (
13.260301515151516,
1.1,
"25",
73,
12.4107195879539,
17.7460505050505,
17.7460505050505,
),
"mathematiques": (
11.142850000000001,
0.13333333333333333,
"39",
73,
11.320606952054794,
19.75,
19.75,
),
"dut": (
13.54367375,
1.0,
"25",
73,
12.730581289342638,
18.226902529333334,
18.226902529333334,
),
},
},
"4S": {
"groupe": {
"informatique": (
14.84359375,
0.5333333333333333,
"2",
19,
10.69933552631579,
18.28646875,
18.28646875,
),
"pe": (
12.93828572598162,
3.75,
"4",
19,
11.861967145815218,
15.737718967605682,
15.737718967605682,
),
"mathematiques": (None, None, "1 ex", 19, None, None, None),
"ptut": (None, None, "1 ex", 19, None, None, None),
"dut": (
13.511767410105122,
4.0,
"4",
19,
12.573349864933606,
15.781651391587998,
15.781651391587998,
),
},
"promo": {
"informatique": (
16.075,
0.1,
"4",
73,
10.316541095890413,
19.333333333333336,
19.333333333333336,
),
"pe": (
13.52416666666667,
0.49999999999999994,
"13",
73,
11.657102668465479,
16.853208080808084,
16.853208080808084,
),
"mathematiques": (
None,
None,
"55 ex",
73,
7.705091805555555,
19.8,
19.8,
),
"dut": (
14.425416666666665,
1.0,
"12",
73,
13.188168241098825,
16.612613522048612,
16.612613522048612,
),
},
},
"S4": {
"groupe": {
"informatique": (
16.075,
0.1,
"1",
19,
8.799078947368422,
16.075,
16.075,
),
"technique": (
13.835576923076923,
0.4333333333333333,
"4",
19,
12.238304655870447,
16.521153846153847,
16.521153846153847,
),
"pe": (
13.52416666666667,
0.49999999999999994,
"4",
19,
12.292846491228072,
16.25833333333334,
16.25833333333334,
),
"dut": (
14.425416666666665,
1.0,
"6",
19,
13.628367861842106,
15.267566666666665,
15.267566666666665,
),
},
"promo": {
"informatique": (
16.075,
0.1,
"4",
73,
10.316541095890413,
19.333333333333336,
19.333333333333336,
),
"pe": (
13.52416666666667,
0.49999999999999994,
"13",
73,
11.657102668465479,
16.853208080808084,
16.853208080808084,
),
"technique": (
13.835576923076923,
0.4333333333333333,
"11",
73,
12.086685508009952,
17.25909420289855,
17.25909420289855,
),
"mathematiques": (
None,
None,
"55 ex",
73,
7.705091805555555,
19.8,
19.8,
),
"ptut": (
13.5,
0.13333333333333333,
"50",
73,
13.898173515981734,
17.083333333333332,
17.083333333333332,
),
"dut": (
14.425416666666665,
1.0,
"12",
73,
13.188168241098825,
16.612613522048612,
16.612613522048612,
),
},
},
"1A": {
"groupe": {
"informatique": (
14.43673913043478,
0.38333333333333336,
"16",
78,
11.046040002787066,
18.85992173913043,
18.85992173913043,
),
"technique": (
13.754459142857144,
1.25,
"14",
78,
11.179785631638866,
18.493250340136054,
18.493250340136054,
),
"pe": (
12.633767581547854,
2.216666666666667,
"18",
78,
10.912253971396854,
18.39547581699347,
18.39547581699347,
),
"mathematiques": (
11.617342857142857,
0.23333333333333334,
"24",
78,
9.921286855287565,
19.375000000000004,
19.375000000000004,
),
"dut": (
12.990587518622945,
2.0,
"18",
78,
11.2117147027821,
18.391345156695156,
18.391345156695156,
),
},
"promo": {
"informatique": (
13.184230769230767,
0.21666666666666667,
"25",
73,
11.696187214611871,
18.51346153846154,
18.51346153846154,
),
"technique": (
12.975409073359078,
0.6166666666666666,
"23",
73,
11.862307379173147,
17.616047267953675,
17.616047267953675,
),
"pe": (
12.016584900684544,
1.116666666666667,
"28",
73,
11.571004424603757,
16.706338951857248,
16.706338951857248,
),
"mathematiques": (
12.25,
0.1,
"18 ex",
73,
10.00886454908676,
19.0625,
19.0625,
),
"dut": (
12.43750128724589,
1.0,
"25",
73,
11.88798432763965,
17.397627309377608,
17.397627309377608,
),
},
},
"2A": {
"groupe": {
"informatique": (
15.88333333333333,
0.15000000000000002,
"2",
19,
9.805818713450288,
17.346666666666668,
17.346666666666668,
),
"pe": (
13.378513043478259,
1.5333333333333334,
"6",
19,
12.099566454042717,
16.06209927536232,
16.06209927536232,
),
"technique": (
13.965093333333336,
1.1666666666666665,
"5",
19,
12.51068332957394,
16.472092380952386,
16.472092380952386,
),
"mathematiques": (None, None, "1 ex", 19, None, None, None),
"dut": (
14.032947301587301,
2.0,
"4",
19,
13.043386086541773,
15.574706269841268,
15.574706269841268,
),
},
"promo": {
"informatique": (
16.075,
0.1,
"4",
73,
10.316541095890413,
19.333333333333336,
19.333333333333336,
),
"pe": (
13.52416666666667,
0.49999999999999994,
"13",
73,
11.657102668465479,
16.853208080808084,
16.853208080808084,
),
"technique": (
13.835576923076923,
0.4333333333333333,
"11",
73,
12.086685508009952,
17.25909420289855,
17.25909420289855,
),
"mathematiques": (
None,
None,
"55 ex",
73,
7.705091805555555,
19.8,
19.8,
),
"dut": (
14.425416666666665,
1.0,
"12",
73,
13.188168241098825,
16.612613522048612,
16.612613522048612,
),
},
},
"nbSemestres": 4,
"code_nip": "21414563",
"prenom": "Baptiste",
"age": "21",
"lycee": "PONCET",
"3S": {
"groupe": {
"informatique": (
14.559423076923077,
0.43333333333333335,
"3",
19,
11.137856275303646,
18.8095,
18.8095,
),
"pe": (
12.84815019664546,
3.25,
"4",
19,
11.795678015751701,
15.657624449801428,
15.657624449801428,
),
"technique": (
13.860638395358142,
1.9833333333333334,
"3",
19,
12.395950358235925,
17.340302131732695,
17.340302131732695,
),
"mathematiques": (
11.494044444444445,
0.3,
"6",
19,
9.771571754385965,
14.405358333333334,
14.405358333333334,
),
"dut": (
13.207217657917942,
3.0,
"4",
19,
12.221677199297439,
15.953012966561774,
15.953012966561774,
),
},
"promo": {
"informatique": (15.5, 0.05, "13", 73, 10.52222222222222, 20.0, 20.0),
"pe": (
13.308035483870967,
1.0333333333333334,
"17",
73,
11.854843423685786,
16.191317607526884,
16.191317607526884,
),
"technique": (
14.041625757575758,
0.7333333333333333,
"10",
73,
11.929466899200335,
16.6400384469697,
16.6400384469697,
),
"mathematiques": (
11.0625,
0.06666666666666667,
"40",
73,
11.418430205479451,
19.53,
19.53,
),
"dut": (
13.640477936507937,
1.0,
"14",
73,
12.097377866597594,
16.97088994741667,
16.97088994741667,
),
},
},
"bac": "STI2D",
"S3": {
"groupe": {
"informatique": (15.5, 0.05, "5", 19, 12.842105263157896, 20.0, 20.0),
"pe": (
13.308035483870967,
1.0333333333333334,
"8",
19,
12.339608902093943,
15.967147311827956,
15.967147311827956,
),
"technique": (
14.041625757575758,
0.7333333333333333,
"7",
19,
13.128539816586922,
16.44310151515152,
16.44310151515152,
),
"mathematiques": (
11.0625,
0.06666666666666667,
"6",
19,
9.280921052631578,
16.125,
16.125,
),
"dut": (
13.640477936507937,
1.0,
"8",
19,
12.83638061385213,
15.881845873015871,
15.881845873015871,
),
},
"promo": {
"informatique": (15.5, 0.05, "13", 73, 10.52222222222222, 20.0, 20.0),
"pe": (
13.308035483870967,
1.0333333333333334,
"17",
73,
11.854843423685786,
16.191317607526884,
16.191317607526884,
),
"technique": (
14.041625757575758,
0.7333333333333333,
"10",
73,
11.929466899200335,
16.6400384469697,
16.6400384469697,
),
"mathematiques": (
11.0625,
0.06666666666666667,
"40",
73,
11.418430205479451,
19.53,
19.53,
),
"dut": (
13.640477936507937,
1.0,
"14",
73,
12.097377866597594,
16.97088994741667,
16.97088994741667,
),
},
},
"parcours": [
{
"nom_semestre_dans_parcours": "semestre 4 FAP 2016",
"titreannee": "DUT RT UFA (PPN 2013), semestre 4 FAP 2016",
},
{
"nom_semestre_dans_parcours": "semestre 3 FAP 2015-2016",
"titreannee": "DUT RT UFA (PPN 2013), semestre 3 FAP 2015-2016",
},
{
"nom_semestre_dans_parcours": "semestre 2 FI 2015",
"titreannee": "DUT RT, semestre 2 FI 2015",
},
{
"nom_semestre_dans_parcours": "semestre 1 FI 2014-2015",
"titreannee": "DUT RT, semestre 1 FI 2014-2015",
},
],
}
}

150
app/pe/pe_trajectoire.py Normal file
View File

@ -0,0 +1,150 @@
import app.pe.pe_comp as pe_tools
from app.models import FormSemestre
from app.pe.pe_etudiant import EtudiantsJuryPE, get_dernier_semestre_en_date
class Trajectoire:
"""Modélise, pour un aggrégat visé (par ex: 'S2', '3S', '2A')
et un ensemble d'étudiants donnés,
la combinaison des formsemestres des étudiants amenant à un semestre
terminal visé.
Si l'aggrégat est un semestre de type Si, elle stocke le (ou les)
formsemestres de numéro i qu'ont suivis l'étudiant pour atteindre le Si
(en général 1 si personnes n'a redoublé, mais 2 s'il y a des redoublants)
Pour des aggrégats de type iS ou iA (par ex, 3A=S1+S2+S3), elle identifie
les semestres que les étudiants ont suivis pour les amener jusqu'au semestre
terminal de la trajectoire (par ex: ici un S3).
Ces semestres peuvent être :
* des S1+S2+S1+S2+S3 si redoublement de la 1ère année
* des S1+S2+(année de césure)+S3 si césure, ...
"""
def __init__(self, nom_aggregat: str, semestre_final: FormSemestre):
"""Modélise un ensemble de formsemestres d'étudiants
amenant à un semestre terminal
Args:
nom_aggregat: Un nom d'aggrégat (par ex: '5S')
semestre_final: Le semestre final de l'aggrégat
"""
self.nom = nom_aggregat
self.semestre_final = semestre_final
self.trajectoire_id = (nom_aggregat, semestre_final.formsemestre_id)
"""Les semestres à aggréger"""
self.semestres_aggreges = {}
def add_semestres_a_aggreger(self, semestres: dict[int: FormSemestre]):
"""Ajoute des semestres au semestre à aggréger
Args:
semestres: Dictionnaire ``{fid: FormSemestre(fid)} à ajouter``
"""
self.semestres_aggreges = self.semestres_aggreges | semestres
def get_repr(self):
"""Représentation textuelle d'une trajectoire
basée sur ses semestres aggrégés"""
noms = []
for fid in self.semestres_aggreges:
semestre = self.semestres_aggreges[fid]
noms.append(f"S{semestre.semestre_id}({fid})")
noms = sorted(noms)
repr = f"{self.nom} ({self.semestre_final.formsemestre_id}) {self.semestre_final.date_fin.year}"
if noms:
repr += " - " + "+".join(noms)
return repr
class TrajectoiresJuryPE:
"""Centralise toutes les trajectoires du jury PE"""
def __init__(self, annee_diplome: int):
"""
Args:
annee_diplome: L'année de diplomation
"""
self.annee_diplome = annee_diplome
"""Toutes les trajectoires possibles"""
self.trajectoires: dict[tuple: Trajectoire] = {}
"""Quelle trajectoires pour quel étudiant :
dictionnaire {etudid: {nom_aggregat: Trajectoire}}"""
self.suivi: dict[int: str] = {}
def cree_trajectoires(self, etudiants: EtudiantsJuryPE):
"""Créé toutes les trajectoires, au regard du cursus des étudiants
analysés + les mémorise dans les données de l'étudiant
"""
for nom_aggregat in pe_tools.TOUS_LES_SEMESTRES + pe_tools.TOUS_LES_AGGREGATS:
"""L'aggrégat considéré (par ex: 3S=S1+S2+S3), son nom de son semestre terminal (par ex: S3) et son numéro (par ex: 3)"""
noms_semestre_de_aggregat = pe_tools.PARCOURS[nom_aggregat]["aggregat"]
nom_semestre_terminal = noms_semestre_de_aggregat[-1]
for etudid in etudiants.cursus:
if etudid not in self.suivi:
self.suivi[etudid] = {aggregat: None
for aggregat in pe_tools.TOUS_LES_SEMESTRES + pe_tools.TOUS_LES_AGGREGATS}
"""Le formsemestre terminal (dernier en date) associé au
semestre marquant la fin de l'aggrégat
(par ex: son dernier S3 en date)"""
semestres = etudiants.cursus[etudid][nom_semestre_terminal]
if semestres:
formsemestre_final = get_dernier_semestre_en_date(semestres)
"""Ajout ou récupération de la trajectoire"""
trajectoire_id = (nom_aggregat, formsemestre_final.formsemestre_id)
if trajectoire_id not in self.trajectoires:
trajectoire = Trajectoire(nom_aggregat, formsemestre_final)
self.trajectoires[trajectoire_id] = trajectoire
else:
trajectoire = self.trajectoires[trajectoire_id]
"""La liste des semestres de l'étudiant à prendre en compte
pour cette trajectoire"""
semestres_a_aggreger = etudiants.get_trajectoire(etudid, formsemestre_final)
"""Ajout des semestres à la trajectoire"""
trajectoire.add_semestres_a_aggreger(semestres_a_aggreger)
"""Mémoire la trajectoire suivie par l'étudiant"""
self.suivi[etudid][nom_aggregat] = trajectoire
"""Vérifications"""
# dernier_semestre_aggregat = get_dernier_semestre_en_date(semestres_aggreges)
# assert dernier_semestre_aggregat == formsemestre_terminal
def get_semestres_a_aggreger(self, aggregat: str, formsemestre_id_terminal: int):
"""Pour un nom d'aggrégat donné (par ex: 'S3') et un semestre terminal cible
identifié par son formsemestre_id (par ex: 'S3 2022-2023'),
renvoie l'ensemble des semestres à prendre en compte dans
l'aggrégat sous la forme d'un dictionnaire {fid: FormSemestre(fid)}.
Fusionne les cursus individuels des étudiants, dont le cursus correspond
à l'aggrégat visé.
Args:
aggregat: Un aggrégat (par ex. 1A, 2A, 3S, 6S)
formsemestre_id_terminal: L'identifiant du formsemestre terminal de l'aggrégat, devant correspondre au
dernier semestre de l'aggrégat
"""
noms_semestres_aggreges = pe_tools.PARCOURS[aggregat]["aggregat"]
formsemestres = {}
for etudid in self.cursus:
cursus_etudiant = self.cursus[etudid][aggregat]
if formsemestre_id_terminal in cursus_etudiant:
formsemestres_etudiant = cursus_etudiant[formsemestre_id_terminal]
formsemestres = formsemestres | formsemestres_etudiant
return formsemestres

217
app/pe/pe_trajectoiretag.py Normal file
View File

@ -0,0 +1,217 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
##############################################################################
# Module "Avis de poursuite d'étude"
# conçu et développé par Cléo Baras (IUT de Grenoble)
##############################################################################
"""
Created on Fri Sep 9 09:15:05 2016
@author: barasc
"""
from app.comp import moy_sem
from app.comp.res_sem import load_formsemestre_results
from app.pe.pe_semtag import SemestreTag
from app.pe import pe_tabletags
import pandas as pd
import numpy as np
from app.pe.pe_trajectoire import Trajectoire
from app.pe.pe_etudiant import EtudiantsJuryPE
from app.pe.pe_tabletags import TableTag
class TrajectoireTag(TableTag):
"""Calcule les moyennes par tag d'une combinaison de semestres
(trajectoires), identifiée par un nom d'aggrégat (par ex: '3S') et
par un semestre terminal, pour extraire les classements par tag pour un
groupe d'étudiants donnés. Le groupe d'étudiants est formé par ceux ayant tous
participé au semestre terminal.
Par ex: fusion d'un parcours ['S1', 'S2', 'S3'] donnant un nom_combinaison = '3S'
"""
# -------------------------------------------------------------------------------------------------------------------
def __init__(
self,
nom: str,
trajectoire: Trajectoire,
semestres_taggues: dict[int, SemestreTag],
donnees_etudiants: EtudiantsJuryPE,
):
""" """
TableTag.__init__(self, nom=nom)
"""La trajectoire associée"""
self.trajectoire_id = trajectoire.trajectoire_id
self.trajectoire = trajectoire
"""Le formsemestre terminal et les semestres aggrégés"""
self.formsemestre_terminal = trajectoire.semestre_final
nt = load_formsemestre_results(self.formsemestre_terminal)
self.semestres_aggreges = trajectoire.semestres_aggreges
"""Les semestres tags associés aux semestres aggrégés"""
try:
self.semestres_tags_aggreges = {
frmsem_id: semestres_taggues[frmsem_id]
for frmsem_id in semestres_taggues
}
except:
raise ValueError("Semestres taggués manquants")
"""Les étudiants (état civil + cursus connu)"""
self.etuds = nt.etuds
# assert self.etuds == trajectoire.suivi # manque-t-il des étudiants ?
self.etudiants = {etud.etudid: etud.etat_civil for etud in self.etuds}
"""Les tags extraits de tous les semestres"""
self.tags_sorted = self.do_taglist()
"""Construit le cube de notes"""
self.notes_cube = self.compute_notes_cube()
"""Calcul les moyennes par tag sous forme d'un dataframe"""
etudids = list(self.etudiants.keys())
self.notes = compute_tag_moy(self.notes_cube, etudids, self.tags_sorted)
"""Synthétise les moyennes/classements par tag"""
self.moyennes_tags = {}
for tag in self.tags_sorted:
moy_gen_tag = self.notes[tag]
class_gen_tag = moy_sem.comp_ranks_series(moy_gen_tag)[1] # en int
self.moyennes_tags[tag] = {
"notes": moy_gen_tag,
"classements": class_gen_tag,
"min": moy_gen_tag.min(),
"max": moy_gen_tag.max(),
"moy": moy_gen_tag.mean(),
"nb_inscrits": len(moy_gen_tag),
}
def get_repr(self):
"""Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle
est basée)"""
return self.trajectoire.get_repr()
def compute_notes_cube(self):
"""Construit le cube de notes (etudid x tags x semestre_aggregé)
nécessaire au calcul des moyennes de l'aggrégat
"""
nb_tags = len(self.tags_sorted)
nb_etudiants = len(self.etuds)
nb_semestres = len(self.semestres_tags_aggreges)
"""Index du cube (etudids -> dim 0, tags -> dim 1)"""
etudids = [etud.etudid for etud in self.etuds]
tags = self.tags_sorted
semestres_id = list(self.semestres_tags_aggreges.keys())
dfs = {}
for frmsem_id in semestres_id:
"""Partant d'un dataframe vierge"""
df = pd.DataFrame(np.nan, index=etudids, columns=tags)
"""Charge les notes du semestre tag"""
notes = self.semestres_tags_aggreges[frmsem_id].notes
"""Les étudiants & les tags commun au dataframe final et aux notes du semestre)"""
etudids_communs = df.index.intersection(notes.index)
tags_communs = df.columns.intersection(notes.columns)
"""Injecte les notes par tag"""
df.loc[etudids_communs, tags_communs] = notes.loc[
etudids_communs, tags_communs
]
"""Stocke le df"""
dfs[frmsem_id] = df
"""Réunit les notes sous forme d'un cube etdids x tags x semestres"""
semestres_x_etudids_x_tags = [dfs[fid].values for fid in dfs]
etudids_x_tags_x_semestres = np.stack(semestres_x_etudids_x_tags, axis=-1)
return etudids_x_tags_x_semestres
def do_taglist(self):
"""Synthétise les tags à partir des semestres (taggués) aggrégés
Returns:
Une liste de tags triés par ordre alphabétique
"""
tags = []
for frmsem_id in self.semestres_tags_aggreges:
tags.extend(self.semestres_tags_aggreges[frmsem_id].tags_sorted)
return sorted(set(tags))
def compute_tag_moy(set_cube: np.array, etudids: list, tags: list):
"""Calcul de la moyenne par tag sur plusieurs semestres.
La moyenne est un nombre (note/20), ou NaN si pas de notes disponibles
*Remarque* : Adaptation de moy_ue.compute_ue_moys_apc au cas des moyennes de tag
par aggrégat de plusieurs semestres.
Args:
set_cube: notes moyennes aux modules ndarray
(etuds x modimpls x UEs), des floats avec des NaN
etudids: liste des étudiants (dim. 0 du cube)
tags: liste des tags (dim. 1 du cube)
Returns:
Un DataFrame avec pour columns les moyennes par tags,
et pour rows les etudid
"""
nb_etuds, nb_tags, nb_semestres = set_cube.shape
assert nb_etuds == len(etudids)
assert nb_tags == len(tags)
# Quelles entrées du cube contiennent des notes ?
mask = ~np.isnan(set_cube)
# Enlève les NaN du cube pour les entrées manquantes
set_cube_no_nan = np.nan_to_num(set_cube, nan=0.0)
# Les moyennes par tag
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
etud_moy_tag = np.sum(set_cube_no_nan, axis=2) / np.sum(mask, axis=2)
# Le dataFrame
etud_moy_tag_df = pd.DataFrame(
etud_moy_tag,
index=etudids, # les etudids
columns=tags, # les tags
)
return etud_moy_tag_df

View File

@ -36,6 +36,8 @@
"""
from flask import send_file, request
from app.models import FormSemestre
from app.scodoc.sco_exceptions import ScoValueError
import app.scodoc.sco_utils as scu
@ -43,18 +45,44 @@ from app.scodoc import sco_formsemestre
from app.scodoc import html_sco_header
from app.scodoc import sco_preferences
from app.pe import pe_tools
from app.pe import pe_jurype
from app.pe import pe_comp
from app.pe import pe_jury
from app.pe import pe_avislatex
def _pe_view_sem_recap_form(formsemestre_id):
sem_base = FormSemestre.get_formsemestre(formsemestre_id)
if not sem_base.formation.is_apc() or sem_base.formation.get_cursus().NB_SEM < 6:
H = [
html_sco_header.sco_header(page_title="Avis de poursuite d'études"),
f"""<h2 class="formsemestre">Génération des avis de poursuites d'études</h2>
<p class="help">
Cette fonction génère un ensemble de fichiers permettant d'éditer des avis de
poursuites d'études.
<br>
De nombreux aspects sont paramétrables:
<a href="https://scodoc.org/AvisPoursuiteEtudes"
target="_blank" rel="noopener noreferrer">
voir la documentation
</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()
# L'année du diplome
diplome = pe_comp.get_annee_diplome_semestre(sem_base)
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>
<p class="help">
Cette fonction génère un ensemble de fichiers permettant d'éditer des avis de
poursuites d'études.
poursuites d'études pour les étudiants diplômés en {diplome}.
<br>
De nombreux aspects sont paramétrables:
<a href="https://scodoc.org/AvisPoursuiteEtudes"
@ -96,87 +124,103 @@ def pe_view_sem_recap(
return _pe_view_sem_recap_form(formsemestre_id)
prefs = sco_preferences.SemPreferences(formsemestre_id=formsemestre_id)
semBase = sco_formsemestre.get_formsemestre(formsemestre_id)
sem_base = FormSemestre.get_formsemestre(formsemestre_id)
if not sem_base.formation.is_apc():
raise ScoValueError(
"Le module de Poursuites d'Etudes avec Scodoc 9 n'est disponible que pour des formations BUT"
)
if sem_base.formation.get_cursus().NB_SEM < 6:
raise ScoValueError(
"Le module de Poursuites d'Etudes avec Scodoc 9 n'est pas prévu pour une formation de moins de 6 semestres"
)
# L'année du diplome
diplome = pe_comp.get_annee_diplome_semestre(sem_base)
jury = pe_jury.JuryPE(diplome, sem_base.formation.formation_id)
jury = pe_jurype.JuryPE(semBase)
# Ajout avis LaTeX au même zip:
etudids = list(jury.syntheseJury.keys())
# etudids = list(jury.syntheseJury.keys())
# Récupération du template latex, du footer latex et du tag identifiant les annotations relatives aux PE
# (chaines unicodes, html non quoté)
template_latex = ""
# template fourni via le formulaire Web
if avis_tmpl_file:
try:
template_latex = avis_tmpl_file.read().decode("utf-8")
except UnicodeDecodeError as e:
raise ScoValueError(
"Données (template) invalides (caractères non UTF8 ?)"
) from e
else:
# template indiqué dans préférences ScoDoc ?
template_latex = pe_avislatex.get_code_latex_from_scodoc_preference(
formsemestre_id, champ="pe_avis_latex_tmpl"
)
if False:
if avis_tmpl_file:
try:
template_latex = avis_tmpl_file.read().decode("utf-8")
except UnicodeDecodeError as e:
raise ScoValueError(
"Données (template) invalides (caractères non UTF8 ?)"
) from e
else:
# template indiqué dans préférences ScoDoc ?
template_latex = pe_avislatex.get_code_latex_from_scodoc_preference(
formsemestre_id, champ="pe_avis_latex_tmpl"
)
template_latex = template_latex.strip()
if not template_latex:
# pas de preference pour le template: utilise fichier du serveur
template_latex = pe_avislatex.get_templates_from_distrib("avis")
template_latex = template_latex.strip()
if not template_latex:
# pas de preference pour le template: utilise fichier du serveur
template_latex = pe_avislatex.get_templates_from_distrib("avis")
# Footer:
footer_latex = ""
# template fourni via le formulaire Web
if footer_tmpl_file:
footer_latex = footer_tmpl_file.read().decode("utf-8")
else:
footer_latex = pe_avislatex.get_code_latex_from_scodoc_preference(
formsemestre_id, champ="pe_avis_latex_footer"
)
footer_latex = footer_latex.strip()
if not footer_latex:
# pas de preference pour le footer: utilise fichier du serveur
footer_latex = pe_avislatex.get_templates_from_distrib(
"footer"
) # fallback: footer vides
# Footer:
footer_latex = ""
# template fourni via le formulaire Web
if footer_tmpl_file:
footer_latex = footer_tmpl_file.read().decode("utf-8")
else:
footer_latex = pe_avislatex.get_code_latex_from_scodoc_preference(
formsemestre_id, champ="pe_avis_latex_footer"
)
footer_latex = footer_latex.strip()
if not footer_latex:
# pas de preference pour le footer: utilise fichier du serveur
footer_latex = pe_avislatex.get_templates_from_distrib(
"footer"
) # fallback: footer vides
tag_annotation_pe = pe_avislatex.get_code_latex_from_scodoc_preference(
formsemestre_id, champ="pe_tag_annotation_avis_latex"
)
# Ajout des annotations PE dans un fichier excel
sT = pe_avislatex.table_syntheseAnnotationPE(jury.syntheseJury, tag_annotation_pe)
if sT:
jury.add_file_to_zip(
jury.NOM_EXPORT_ZIP + "_annotationsPE" + scu.XLSX_SUFFIX, sT.excel()
# sT = pe_avislatex.table_syntheseAnnotationPE(jury.syntheseJury, tag_annotation_pe)
# if sT:
# jury.add_file_to_zip(
# jury.nom_export_zip + "_annotationsPE" + scu.XLSX_SUFFIX, sT.excel()
# )
if False:
latex_pages = {} # Dictionnaire de la forme nom_fichier => contenu_latex
for etudid in etudids:
[nom_fichier, contenu_latex] = pe_avislatex.get_avis_poursuite_par_etudiant(
jury,
etudid,
template_latex,
tag_annotation_pe,
footer_latex,
prefs,
)
jury.add_file_to_zip("avis/" + nom_fichier + ".tex", contenu_latex)
latex_pages[nom_fichier] = contenu_latex # Sauvegarde dans un dico
# Nouvelle version : 1 fichier par étudiant avec 1 fichier appelant créée ci-dessous
doc_latex = "\n% -----\n".join(
["\\include{" + nom + "}" for nom in sorted(latex_pages.keys())]
)
jury.add_file_to_zip("avis/avis_poursuite.tex", doc_latex)
latex_pages = {} # Dictionnaire de la forme nom_fichier => contenu_latex
for etudid in etudids:
[nom_fichier, contenu_latex] = pe_avislatex.get_avis_poursuite_par_etudiant(
jury,
etudid,
template_latex,
tag_annotation_pe,
footer_latex,
prefs,
)
jury.add_file_to_zip("avis/" + nom_fichier + ".tex", contenu_latex)
latex_pages[nom_fichier] = contenu_latex # Sauvegarde dans un dico
# Ajoute image, LaTeX class file(s) and modeles
pe_tools.add_pe_stuff_to_zip(jury.zipfile, jury.nom_export_zip)
# Nouvelle version : 1 fichier par étudiant avec 1 fichier appelant créée ci-dessous
doc_latex = "\n% -----\n".join(
["\\include{" + nom + "}" for nom in sorted(latex_pages.keys())]
)
jury.add_file_to_zip("avis/avis_poursuite.tex", doc_latex)
# Ajoute image, LaTeX class file(s) and modeles
pe_tools.add_pe_stuff_to_zip(jury.zipfile, jury.NOM_EXPORT_ZIP)
data = jury.get_zipped_data()
return send_file(
data,
mimetype="application/zip",
download_name=scu.sanitize_filename(jury.NOM_EXPORT_ZIP + ".zip"),
download_name=scu.sanitize_filename(jury.nom_export_zip + ".zip"),
as_attachment=True,
)

View File

@ -292,24 +292,35 @@ def get_etud_tagged_modules(etudid, tagname):
return R
def split_tagname_coeff(tag, separateur=":"):
"""Découpe un tag saisi par un utilisateur pour en extraire un tagname
(chaine de caractère correspondant au tag)
et un éventuel coefficient de pondération, avec le séparateur fourni (par défaut ":").
Renvoie le résultat sous la forme d'une liste [tagname, pond] où pond est un float
def split_tagname_coeff(tag: str, separateur=":") -> tuple[str, float]:
"""Découpage d'un tag, tel que saisi par un utilisateur dans le programme,
pour en extraire :
Auteur: CB
* son _nom de tag_ (tagname) (chaine de caractère correspondant au tag)
* un éventuel coefficient de pondération, avec le séparateur fourni (par défaut ":").
Args:
tag: La saisie utilisateur du tag dans le programme
separateur: Le séparateur des informations dans la saisie utilisateur
Return:
Tuple (tagname, coeff_de_ponderation) extrait de la saisie utilisateur
(avec coeff_de_ponderation=1.0 si non mentionné)
Author:
Cléo Baras
"""
if separateur in tag:
temp = tag.split(":")
try:
pond = float(temp[1])
return [temp[0], pond]
return (temp[0], pond)
except:
return [tag, 1.0] # renvoie tout le tag si le découpage à échouer
"""Renvoie tout le tag si le découpage à échouer"""
return (tag, 1.0)
else:
# initialise le coeff de pondération à 1 lorsqu'aucun coeff de pondération n'est indiqué dans le tag
return [tag, 1.0]
"""initialise le coeff de pondération à 1 lorsqu'aucun coeff de pondération n'est indiqué dans le tag"""
return (tag, 1.0)
"""Tests: