forked from ScoDoc/ScoDoc
Merge pe-BUT-v2 de Cléo
This commit is contained in:
commit
3a3d47ebe4
68
app/pe/pe_affichage.py
Normal file
68
app/pe/pe_affichage.py
Normal 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
|
@ -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
384
app/pe/pe_comp.py
Normal 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
527
app/pe/pe_etudiant.py
Normal 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 là, 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
125
app/pe/pe_interclasstag.py
Normal 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
480
app/pe/pe_jury.py
Normal 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
|
1274
app/pe/pe_jurype.py
1274
app/pe/pe_jurype.py
File diff suppressed because it is too large
Load Diff
@ -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
|
@ -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
|
@ -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=";")
|
||||
|
||||
|
||||
# ************************************************************************
|
@ -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
150
app/pe/pe_trajectoire.py
Normal 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
217
app/pe/pe_trajectoiretag.py
Normal 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
|
@ -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,
|
||||
)
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user