Presque la version 2?

This commit is contained in:
Cléo Baras 2024-01-25 17:17:01 +01:00
parent 82713752c2
commit decc28b896
12 changed files with 992 additions and 2045 deletions

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

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

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

@ -0,0 +1,384 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
##############################################################################
# Module "Avis de poursuite d'étude"
# conçu et développé par Cléo Baras (IUT de Grenoble)
##############################################################################
"""
Created on Thu Sep 8 09:36:33 2016
@author: barasc
"""
import os
import datetime
import re
import unicodedata
from flask import g
import app.scodoc.sco_utils as scu
from app import log
from app.models import FormSemestre
from app.scodoc import sco_formsemestre
from app.scodoc.sco_logos import find_logo
PE_DEBUG = 1
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

View File

@ -36,9 +36,10 @@ Created on 17/01/2024
@author: barasc @author: barasc
""" """
import app.pe.pe_tools as pe_tools import app.pe.pe_comp as pe_comp
from app.models import FormSemestre, Identite from app.models import FormSemestre, Identite
from app.pe.pe_tools import pe_print from app.pe.pe_comp import pe_print
class EtudiantsJuryPE: class EtudiantsJuryPE:
"""Classe centralisant la gestion des étudiants à prendre en compte dans un jury de PE""" """Classe centralisant la gestion des étudiants à prendre en compte dans un jury de PE"""
@ -79,22 +80,22 @@ class EtudiantsJuryPE:
*Remarque* : ex: JuryPE.get_etudiants_in_jury() *Remarque* : ex: JuryPE.get_etudiants_in_jury()
""" """
"Les cosemestres donnant lieu à même année de diplome" "Les cosemestres donnant lieu à même année de diplome"
cosemestres = pe_tools.get_cosemestres_diplomants(self.annee_diplome, None) cosemestres = pe_comp.get_cosemestres_diplomants(self.annee_diplome, None)
self.cosemestres = cosemestres self.cosemestres = cosemestres
pe_tools.pe_print( pe_comp.pe_print(
"1) Recherche des coSemestres -> %d trouvés" % len(cosemestres) "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)""" """Les étudiants inscrits dans les co-semestres (ceux du jury mais aussi d'autres ayant été réorientés ou ayant abandonnés)"""
pe_tools.pe_print("2) Liste des étudiants dans les différents co-semestres") pe_comp.pe_print("2) Liste des étudiants dans les différents co-semestres")
self.etudiants_ids = get_etudiants_dans_semestres(cosemestres) self.etudiants_ids = get_etudiants_dans_semestres(cosemestres)
pe_tools.pe_print( pe_comp.pe_print(
" => %d étudiants trouvés dans les cosemestres" % len(self.etudiants_ids) " => %d étudiants trouvés dans les cosemestres" % len(self.etudiants_ids)
) )
"""Analyse des parcours étudiants pour déterminer leur année effective de diplome """Analyse des parcours étudiants pour déterminer leur année effective de diplome
avec prise en compte des redoublements, des abandons, ....""" avec prise en compte des redoublements, des abandons, ...."""
pe_tools.pe_print("3) Analyse des parcours individuels des étudiants") pe_comp.pe_print("3) Analyse des parcours individuels des étudiants")
no_etud = 0 no_etud = 0
for no_etud, etudid in enumerate(self.etudiants_ids): for no_etud, etudid in enumerate(self.etudiants_ids):
@ -109,9 +110,9 @@ class EtudiantsJuryPE:
self.structure_cursus_etudiant(etudid) self.structure_cursus_etudiant(etudid)
if (no_etud + 1) % 10 == 0: if (no_etud + 1) % 10 == 0:
pe_tools.pe_print((no_etud + 1), " ", end="") pe_comp.pe_print((no_etud + 1), " ", end="")
no_etud += 1 no_etud += 1
pe_tools.pe_print() pe_comp.pe_print()
"""Les étudiants à prendre dans le diplôme, étudiants ayant abandonnés non compris""" """Les étudiants à prendre dans le diplôme, étudiants ayant abandonnés non compris"""
self.etudiants_diplomes = self.get_etudiants_diplomes() self.etudiants_diplomes = self.get_etudiants_diplomes()
@ -124,19 +125,19 @@ class EtudiantsJuryPE:
self.formsemestres_jury_ids = self.get_formsemestres() self.formsemestres_jury_ids = self.get_formsemestres()
# Synthèse # Synthèse
pe_tools.pe_print( pe_comp.pe_print(
f" => {len(self.etudiants_diplomes)} étudiants à diplômer en {self.annee_diplome}" f" => {len(self.etudiants_diplomes)} étudiants à diplômer en {self.annee_diplome}"
) )
nbre_abandons = len(self.etudiants_ids) - len(self.etudiants_diplomes) nbre_abandons = len(self.etudiants_ids) - len(self.etudiants_diplomes)
pe_tools.pe_print(f" => {nbre_abandons} étudiants éliminer pour abandon") pe_comp.pe_print(f" => {nbre_abandons} étudiants éliminer pour abandon")
pe_tools.pe_print( pe_comp.pe_print(
f" => {len(self.formsemestres_jury_ids)} semestres dont il faut calculer la moyenne" f" => {len(self.formsemestres_jury_ids)} semestres dont il faut calculer la moyenne"
) )
pe_tools.pe_print( pe_comp.pe_print(
f" => quelques étudiants futurs diplômés : " f" => quelques étudiants futurs diplômés : "
+ ", ".join([str(etudid) for etudid in list(self.etudiants_diplomes)[:10]]) + ", ".join([str(etudid) for etudid in list(self.etudiants_diplomes)[:10]])
) )
pe_tools.pe_print( pe_comp.pe_print(
f" => semestres dont il faut calculer les moyennes : " f" => semestres dont il faut calculer les moyennes : "
+ ", ".join([str(fid) for fid in list(self.formsemestres_jury_ids)]) + ", ".join([str(fid) for fid in list(self.formsemestres_jury_ids)])
) )
@ -183,9 +184,11 @@ class EtudiantsJuryPE:
identite = Identite.get_etud(etudid) identite = Identite.get_etud(etudid)
"""Le cursus global de l'étudiant (restreint aux semestres APC)""" """Le cursus global de l'étudiant (restreint aux semestres APC)"""
formsemestres = identite.get_formsemestres()
semestres_etudiant = { semestres_etudiant = {
frmsem.formsemestre_id: frmsem frmsem.formsemestre_id: frmsem
for frmsem in identite.get_formsemestres() for frmsem in formsemestres
if frmsem.formation.is_apc() if frmsem.formation.is_apc()
} }
@ -193,8 +196,10 @@ class EtudiantsJuryPE:
"etudid": etudid, # les infos sur l'étudiant "etudid": etudid, # les infos sur l'étudiant
"etat_civil": identite.etat_civil, # Ajout à la table jury "etat_civil": identite.etat_civil, # Ajout à la table jury
"nom": identite.nom, "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 "diplome": annee_diplome(identite), # Le date prévisionnelle de son diplôme
"formsemestres": semestres_etudiant, # les semestres de l'étudiant "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 "abandon": False, # va être traité en dessous
} }
@ -218,7 +223,7 @@ class EtudiantsJuryPE:
semestres_significatifs = {} semestres_significatifs = {}
for fid in semestres_etudiant: for fid in semestres_etudiant:
semestre = semestres_etudiant[fid] semestre = semestres_etudiant[fid]
if pe_tools.get_annee_diplome_semestre(semestre) <= self.annee_diplome: if pe_comp.get_annee_diplome_semestre(semestre) <= self.annee_diplome:
semestres_significatifs[fid] = semestre semestres_significatifs[fid] = semestre
return semestres_significatifs return semestres_significatifs
@ -234,7 +239,7 @@ class EtudiantsJuryPE:
semestres_significatifs = self.get_semestres_significatifs(etudid) semestres_significatifs = self.get_semestres_significatifs(etudid)
"""Tri des semestres par numéro de semestre""" """Tri des semestres par numéro de semestre"""
for nom_sem in pe_tools.TOUS_LES_SEMESTRES: for nom_sem in pe_comp.TOUS_LES_SEMESTRES:
i = int(nom_sem[1]) # le n° du semestre i = int(nom_sem[1]) # le n° du semestre
semestres_i = { semestres_i = {
fid: semestres_significatifs[fid] fid: semestres_significatifs[fid]
@ -324,7 +329,7 @@ class EtudiantsJuryPE:
""" """
if semestres_recherches is None: if semestres_recherches is None:
"""Appel récursif pour obtenir tous les semestres (validants)""" """Appel récursif pour obtenir tous les semestres (validants)"""
semestres = self.get_formsemestres(pe_tools.AGGREGAT_DIPLOMANT) semestres = self.get_formsemestres(pe_comp.AGGREGAT_DIPLOMANT)
return semestres return semestres
elif isinstance(semestres_recherches, list): elif isinstance(semestres_recherches, list):
"""Appel récursif sur tous les éléments de la liste""" """Appel récursif sur tous les éléments de la liste"""
@ -335,16 +340,16 @@ class EtudiantsJuryPE:
return semestres return semestres
elif ( elif (
isinstance(semestres_recherches, str) isinstance(semestres_recherches, str)
and semestres_recherches in pe_tools.TOUS_LES_AGGREGATS 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""" """Cas d'un aggrégat avec appel récursif sur toutes les entrées de l'aggrégat"""
semestres = self.get_formsemestres( semestres = self.get_formsemestres(
pe_tools.PARCOURS[semestres_recherches]["aggregat"] pe_comp.PARCOURS[semestres_recherches]["aggregat"]
) )
return semestres return semestres
elif ( elif (
isinstance(semestres_recherches, str) isinstance(semestres_recherches, str)
and semestres_recherches in pe_tools.TOUS_LES_SEMESTRES and semestres_recherches in pe_comp.TOUS_LES_SEMESTRES
): ):
"""semestres_recherches est un nom de semestre de type S1, """semestres_recherches est un nom de semestre de type S1,
pour une recherche parmi les étudiants à prendre en compte pour une recherche parmi les étudiants à prendre en compte
@ -359,6 +364,15 @@ class EtudiantsJuryPE:
else: else:
raise ValueError("Probleme de paramètres d'appel dans get_formsemestreids") 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: def get_etudiants_dans_semestres(semestres: dict[int, FormSemestre]) -> set:
"""Ensemble d'identifiants des étudiants (identifiés via leur ``etudid``) """Ensemble d'identifiants des étudiants (identifiés via leur ``etudid``)
@ -402,7 +416,7 @@ def annee_diplome(identite: Identite) -> int:
if formsemestres: if formsemestres:
return max( return max(
[ [
pe_tools.get_annee_diplome_semestre(sem_base) pe_comp.get_annee_diplome_semestre(sem_base)
for sem_base in formsemestres for sem_base in formsemestres
] ]
) )
@ -459,14 +473,14 @@ def arret_de_formation(identite: Identite, cosemestres: list[FormSemestre]) -> b
# semestre impair => passage de droit en semestre pair suivant (effet de l'annualisation) # semestre impair => passage de droit en semestre pair suivant (effet de l'annualisation)
if numero_dernier_formsemestre % 2 == 1: if numero_dernier_formsemestre % 2 == 1:
numeros_possibles = list( numeros_possibles = list(
range(numero_dernier_formsemestre + 1, pe_tools.NBRE_SEMESTRES_DIPLOMANT) range(numero_dernier_formsemestre + 1, pe_comp.NBRE_SEMESTRES_DIPLOMANT)
) )
# semestre pair => passage en année supérieure ou redoublement # semestre pair => passage en année supérieure ou redoublement
else: # else: #
numeros_possibles = list( numeros_possibles = list(
range( range(
max(numero_dernier_formsemestre - 1, 1), max(numero_dernier_formsemestre - 1, 1),
pe_tools.NBRE_SEMESTRES_DIPLOMANT, pe_comp.NBRE_SEMESTRES_DIPLOMANT,
) )
) )
@ -486,9 +500,11 @@ def arret_de_formation(identite: Identite, cosemestres: list[FormSemestre]) -> b
return False return False
def get_dernier_semestre_en_date(semestres: dict[int, FormSemestre]):
def get_dernier_semestre_en_date(semestres: dict[int, FormSemestre]) -> FormSemestre:
"""Renvoie le dernier semestre en **date de fin** d'un dictionnaire """Renvoie le dernier semestre en **date de fin** d'un dictionnaire
de semestres de la forme ``{fid: FormSemestre(fid)}``. de semestres (potentiellement non trié) de la forme ``{fid: FormSemestre(fid)}``.
Args: Args:
semestres: Un dictionnaire de semestres semestres: Un dictionnaire de semestres
@ -505,3 +521,5 @@ def get_dernier_semestre_en_date(semestres: dict[int, FormSemestre]):
return dernier_semestre return dernier_semestre
else: else:
return None return None

View File

@ -1,8 +1,5 @@
import pandas as pd
from app.pe import pe_tagtable from app.pe.pe_tabletags import TableTag
from app.pe.pe_tools import PE_DEBUG, pe_print
import app.pe.pe_etudiant as pe_etudiant
from app.pe.pe_etudiant import EtudiantsJuryPE from app.pe.pe_etudiant import EtudiantsJuryPE
from app.pe.pe_trajectoire import Trajectoire, TrajectoiresJuryPE from app.pe.pe_trajectoire import Trajectoire, TrajectoiresJuryPE
from app.pe.pe_trajectoiretag import TrajectoireTag from app.pe.pe_trajectoiretag import TrajectoireTag
@ -12,7 +9,7 @@ import pandas as pd
import numpy as np import numpy as np
class AggregatInterclasseTag(pe_tagtable.TableTag): class AggregatInterclasseTag(TableTag):
"""Interclasse l'ensemble des étudiants diplômés à une année """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') donnée (celle du jury), pour un aggrégat donné (par ex: 'S2', '3S')
en reportant : en reportant :
@ -32,7 +29,7 @@ class AggregatInterclasseTag(pe_tagtable.TableTag):
): ):
"""""" """"""
"""Table nommée au nom de l'aggrégat (par ex: 3S""" """Table nommée au nom de l'aggrégat (par ex: 3S"""
pe_tagtable.TableTag.__init__(self, nom_aggregat) TableTag.__init__(self, nom_aggregat)
"""Les étudiants diplômés et leurs trajectoires (cf. trajectoires.suivis)""" """Les étudiants diplômés et leurs trajectoires (cf. trajectoires.suivis)"""
self.diplomes_ids = etudiants.etudiants_diplomes self.diplomes_ids = etudiants.etudiants_diplomes

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

@ -0,0 +1,477 @@
# -*- 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', '', }}``
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
"""
self.promoTagDict = {}
"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) + '.xls'
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.zipfile.write(filename)
"""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(self.nom_export_zip, path, filename)
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')
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
df = pd.DataFrame.from_dict(donnees, orient='index')
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):
"""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

View File

@ -1,736 +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
"""
import datetime
# ----------------------------------------------------------
# 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.comp.res_sem import load_formsemestre_results
from app.models import Formation, FormSemestre
from app.models.etudiants import Identite
from app.pe.pe_semestretag import SemestreTag
from app.pe.pe_interclasstag import AggregatInterclasseTag
from app.pe.pe_trajectoiretag import TrajectoireTag
from app.scodoc.gen_tables import GenTable, SeqGenTable
import app.scodoc.sco_utils as scu
from app.scodoc import codes_cursus
from app.pe import pe_tools
from app.pe import pe_semestretag
from app.pe.pe_etudiant import EtudiantsJuryPE
from app.pe.pe_trajectoire import TrajectoiresJuryPE, Trajectoire
# ----------------------------------------------------------------------------------------
def comp_nom_semestre_dans_parcours(sem):
"""Le nom a afficher pour titrer un semestre
par exemple: "semestre 2 FI 2015"
"""
formation: Formation = Formation.query.get_or_404(sem["formation_id"])
parcours = codes_cursus.get_cursus_from_code(formation.type_parcours)
return "%s %s %s %s" % (
parcours.SESSION_NAME, # eg "semestre"
sem["semestre_id"], # eg 2
sem.get("modalite", ""), # eg FI ou FC
sem["annee_debut"], # eg 2015
)
# ----------------------------------------------------------------------------------------
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', '', }}``
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
"""
self.promoTagDict = {}
"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")
self.syntheseJury = {} # Le jury de synthèse
"""Chargement des étudiants à prendre en compte dans le jury"""
pe_tools.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)
"""Génère les semestres taggués (avec le calcul des moyennes) pour le jury PE"""
pe_tools.pe_print("*** Génère les semestres taggués")
self.semestres_taggues = compute_semestres_tag(self.etudiants)
if pe_tools.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_tools.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_tools.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_tools.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_tools.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_tools.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_tools.pe_print("*** Génère les interclassements par aggrégat")
self.interclassements_taggues = compute_interclassements(
self.etudiants, self.trajectoires, self.trajectoires_tagguees
)
if pe_tools.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_tools.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.synthetise_juryPE()
# Export des données => mode 1 seule feuille -> supprimé
# filename = self.NOM_EXPORT_ZIP + "jurySyntheseDict_" + str(self.diplome) + '.xls'
# self.xls = self.table_syntheseJury(mode="singlesheet")
# self.add_file_to_zip(filename, self.xls.excel())
# Fabrique 1 fichier excel résultat avec 1 seule feuille => trop gros
if False:
filename = self.nom_export_zip + "_jurySyntheseDict" + scu.XLSX_SUFFIX
self.xlsV2 = self.table_syntheseJury(mode="multiplesheet")
if self.xlsV2:
pe_tools.add_file_to_zip(
self.nom_export_zip, filename, self.xlsV2.excel()
)
# Pour debug
# self.syntheseJury = pe_tools.JURY_SYNTHESE_POUR_DEBUG #Un dictionnaire fictif pour debug
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(self.nom_export_zip, path, filename)
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
# **************************************************************************************************************** #
# Traitements des semestres impliqués dans le jury
# **************************************************************************************************************** #
# **************************************************************************************************************** #
# Méthodes pour la synthèse du juryPE
# *****************************************************************************************************************
def synthetise_juryPE(self):
"""Synthétise tous les résultats du jury PE dans des dataframess"""
self.syntheseJury = {}
for etudid in self.etudiants.get_etudids(self.diplome):
etudinfo = self.ETUDINFO_DICT[etudid]
self.syntheseJury[etudid] = {
"nom": etudinfo["nom"],
"prenom": etudinfo["prenom"],
"civilite": etudinfo["civilite"],
"civilite_str": etudinfo["civilite_str"],
"age": str(pe_tools.calcul_age(etudinfo["date_naissance"])),
"lycee": etudinfo["nomlycee"]
+ (
" (" + etudinfo["villelycee"] + ")"
if etudinfo["villelycee"] != ""
else ""
),
"bac": etudinfo["bac"],
"code_nip": etudinfo["code_nip"], # pour la photo
"entree": self.get_dateEntree(etudid),
"promo": self.diplome,
}
# Le parcours
self.syntheseJury[etudid]["parcours"] = self.get_parcoursIUT(
etudid
) # liste des semestres
self.syntheseJury[etudid]["nbSemestres"] = len(
self.syntheseJury[etudid]["parcours"]
) # nombre de semestres
# Ses résultats
for nom in pe_tools.PARCOURS: # S1, puis S2, puis 1A
# dans le groupe : la table tagguée dans les semtag ou les settag si aggrégat
self.syntheseJury[etudid][nom] = {"groupe": {}, "promo": {}}
if (
self.etudiants.cursus[etudid][nom] != None
): # Un parcours valide existe
if nom in pe_tools.TOUS_LES_SEMESTRES:
tagtable = self.semestres_taggues[
self.etudiants.cursus[etudid][nom]
]
else:
tagtable = self.trajectoires_tagguees[nom][
self.etudiants.cursus[etudid][nom]
]
for tag in tagtable.get_all_tags():
self.syntheseJury[etudid][nom]["groupe"][
tag
] = tagtable.get_resultatsEtud(
tag, etudid
) # Le tuple des résultats
# interclassé dans la promo
tagtable = self.promoTagDict[nom]
for tag in tagtable.get_all_tags():
self.syntheseJury[etudid][nom]["promo"][
tag
] = tagtable.get_resultatsEtud(tag, etudid)
def get_dateEntree(self, etudid):
"""Renvoie l'année d'entrée de l'étudiant à l'IUT"""
# etudinfo = self.ETUDINFO_DICT[etudid]
semestres = self.get_semestresBUT_d_un_etudiant(etudid)
if semestres:
# le 1er sem à l'IUT
return semestres[0]["annee_debut"]
else:
return ""
def get_parcoursIUT(self, etudid):
"""Renvoie une liste d'infos sur les semestres du parcours d'un étudiant"""
# etudinfo = self.ETUDINFO_DICT[etudid]
sems = self.etudiants.semestres_etudiant(etudid)
infos = []
for sem in sems:
nomsem = comp_nom_semestre_dans_parcours(sem)
infos.append(
{
"nom_semestre_dans_parcours": nomsem,
"titreannee": sem["titreannee"],
"formsemestre_id": sem["formsemestre_id"], # utile dans le futur ?
}
)
return infos
# **************************************************************************************************************** #
# Méthodes d'affichage pour debug
# **************************************************************************************************************** #
def str_etudiants_in_jury(self, delim=";"):
# En tete:
entete = ["Id", "Nom", "Abandon", "Diplome"]
for nom_sem in pe_tools.TOUS_LES_PARCOURS:
entete += [nom_sem, "descr"]
chaine = delim.join(entete) + "\n"
for etudid in self.etudiants.cursus:
donnees = self.etudiants.cursus[etudid]
# pe_tools.pe_print(etudid, donnees)
# les infos générales
descr = [
etudid,
donnees["nom"],
str(donnees["abandon"]),
str(donnees["diplome"]),
]
# les semestres et les aggrégats
for nom_sem in pe_tools.TOUS_LES_PARCOURS:
table = (
self.semestres_taggues[donnees[nom_sem]].nom
if donnees[nom_sem] in self.semestres_taggues
else "manquant"
)
descr += [
donnees[nom_sem] if donnees[nom_sem] != None else "manquant",
table,
]
chaine += delim.join(descr) + "\n"
return chaine
#
def export_juryPEDict(self):
"""Export csv de self.PARCOURSINFO_DICT"""
fichier = "juryParcoursDict_" + str(self.diplome)
pe_tools.pe_print(" -> Export de " + fichier)
filename = self.nom_export_zip + fichier + ".csv"
self.zipfile.writestr(filename, self.str_etudiants_in_jury())
def get_allTagForAggregat(self, nom_aggregat):
"""Extrait du dictionnaire syntheseJury la liste des tags d'un semestre ou
d'un aggrégat donné par son nom (S1, S2, S3 ou S4, 1A, ...). Renvoie [] si aucun tag.
"""
taglist = set()
for etudid in self.etudiants.get_etudids():
taglist = taglist.union(
set(self.syntheseJury[etudid][nom_aggregat]["groupe"].keys())
)
taglist = taglist.union(
set(self.syntheseJury[etudid][nom_aggregat]["promo"].keys())
)
return list(taglist)
def get_allTagInSyntheseJury(self):
"""Extrait tous les tags du dictionnaire syntheseJury trié par
ordre alphabétique. [] si aucun tag"""
allTags = set()
for nom in pe_tools.TOUS_LES_PARCOURS:
allTags = allTags.union(set(self.get_allTagForAggregat(nom)))
return sorted(list(allTags)) if len(allTags) > 0 else []
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
# Les etudids des étudiants à afficher, triés par ordre alphabétiques de nom+prénom
donnees_tries = sorted(
[
(
etudid,
self.syntheseJury[etudid]["nom"]
+ " "
+ self.syntheseJury[etudid]["prenom"],
)
for etudid in self.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="but",
)
sT.add_genTable("but", T)
return sT
# Si des étudiants
maxParcours = max(
[self.syntheseJury[etudid]["nbSemestres"] for etudid in etudids]
)
infos = ["civilite", "nom", "prenom", "age", "nbSemestres"]
entete = ["etudid"]
entete.extend(infos)
entete.extend(["P%d" % i for i in range(1, maxParcours + 1)])
champs = [
"note",
"class groupe",
"class promo",
"min/moy/max groupe",
"min/moy/max promo",
]
# Les aggrégats à afficher par ordre tel que indiqué dans le dictionnaire parcours
aggregats = list(pe_tools.PARCOURS.keys()) # ['S1', 'S2', ..., '1A', '4S']
# aggregats = sorted(
# aggregats, key=lambda t: pe_tools.PARCOURS[t]["ordre"]
# ) # Tri des aggrégats
if mode == "multiplesheet":
allSheets = (
self.get_allTagInSyntheseJury()
) # tous les tags de syntheseJuryDict
allSheets = sorted(allSheets) # Tri des tags par ordre alphabétique
for sem in pe_tools.TOUS_LES_PARCOURS:
entete.extend(["%s %s" % (sem, champ) for champ in champs])
else: # "singlesheet"
allSheets = ["singlesheet"]
for (
sem
) in (
pe_tools.TOUS_LES_PARCOURS
): # pe_tools.PARCOURS.keys() -> ['S1', 'S2', ..., '1A', '4S']
tags = self.get_allTagForAggregat(sem)
entete.extend(
["%s %s %s" % (sem, tag, champ) for tag in tags for champ in champs]
)
columns_ids = entete # les id et les titres de colonnes sont ici identiques
titles = {i: i for i in columns_ids}
for (
sheet
) in (
allSheets
): # Pour tous les sheets à générer (1 si singlesheet, autant que de tags si multiplesheet)
rows = []
for etudid in etudids:
e = self.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
# if self.syntheseJury[etudid]['nbSemestres'] < maxParcours:
# descr += delim.join( ['']*( maxParcours -self.syntheseJury[etudid]['nbSemestres']) ) + delim
for sem in aggregats: # pe_tools.PARCOURS.keys():
listeTags = (
self.get_allTagForAggregat(sem)
if mode == "singlesheet"
else [sheet]
)
for tag in listeTags:
if tag in self.syntheseJury[etudid][sem]["groupe"]:
resgroupe = self.syntheseJury[etudid][sem]["groupe"][
tag
] # tuple
else:
resgroupe = (None, None, None, None, None, None, None)
if tag in self.syntheseJury[etudid][sem]["promo"]:
respromo = self.syntheseJury[etudid][sem]["promo"][tag]
else:
respromo = (None, None, None, None, None, None, None)
# note = "%2.2f" % resgroupe[0] if isinstance(resgroupe[0], float) else str(resgroupe[0])
champ = (
"%s %s " % (sem, tag)
if mode == "singlesheet"
else "%s " % (sem)
)
row[champ + "note"] = scu.fmt_note(resgroupe[0])
row[champ + "class groupe"] = "%s / %s" % (
resgroupe[2] if resgroupe[2] else "-",
resgroupe[3] if resgroupe[3] else "-",
)
row[champ + "class promo"] = "%s / %s" % (
respromo[2] if respromo[2] else "-",
respromo[3] if respromo[3] else "-",
)
row[champ + "min/moy/max groupe"] = "%s / %s / %s" % tuple(
(scu.fmt_note(x) if x is not None else "-")
for x in (resgroupe[6], resgroupe[4], resgroupe[5])
)
row[champ + "min/moy/max promo"] = "%s / %s / %s" % tuple(
(scu.fmt_note(x) if x is not None else "-")
for x in (respromo[6], respromo[4], respromo[5])
)
rows.append(row)
T = GenTable(
columns_ids=columns_ids,
rows=rows,
titles=titles,
html_sortable=True,
xls_sheet_name=sheet,
)
sT.add_genTable(sheet, T)
if mode == "singlesheet":
return sT.get_genTable("singlesheet")
else:
return sT
# **************************************************************************************************************** #
# Méthodes de classe pour gestion d'un cache de données accélérant les calculs / intérêt à débattre
# **************************************************************************************************************** #
# ------------------------------------------------------------------------------------------------------------------
def get_cache_etudInfo_d_un_etudiant(self, etudid):
"""Renvoie les informations sur le parcours d'un étudiant soit en les relisant depuis
ETUDINFO_DICT si mémorisée soit en les chargeant et en les mémorisant
TODO:: A supprimer à long terme
"""
if etudid not in self.ETUDINFO_DICT:
self.ETUDINFO_DICT[etudid] = Identite.get_etud(etudid=etudid)
# sco_etud.get_etud_info(
# etudid=etudid, filled=True
# ))[0]
return self.ETUDINFO_DICT[etudid]
# ------------------------------------------------------------------------------------------------------------------
# ------------------------------------------------------------------------------------------------------------------
def get_cache_notes_d_un_semestre(self, formsemestre_id: int) -> NotesTableCompat:
"""Charge la table des notes d'un formsemestre"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
return res_sem.load_formsemestre_results(formsemestre)
# ------------------------------------------------------------------------------------------------------------------
# ------------------------------------------------------------------------------------------------------------------
def get_semestresBUT_d_un_etudiant(self, identite: Identite, semestre_id=None):
"""cf. pe_etudiant.semestres_etudiant()"""
return None
# *********************************************
# Fonctions d'affichage pour debug
def get_resultat_d_un_etudiant(self, etudid):
chaine = ""
for nom_sem in pe_tools.TOUS_LES_SEMESTRES:
semtagid = self.etudiants.cursus[etudid][
nom_sem
] # le formsemestre_id du semestre taggué de l'étudiant
semtag = self.semestres_taggues[semtagid]
chaine += "Semestre " + nom_sem + str(semtagid) + "\n"
# le détail du calcul tag par tag
# chaine += "Détail du calcul du tag\n"
# chaine += "-----------------------\n"
# for tag in semtag.taglist:
# chaine += "Tag=" + tag + "\n"
# chaine += semtag.str_detail_resultat_d_un_tag(tag, etudid=etudid) + "\n"
# le bilan des tags
chaine += "Bilan des tags\n"
chaine += "--------------\n"
for tag in semtag.taglist:
chaine += (
tag + ";" + semtag.str_resTag_d_un_etudiant(tag, etudid) + "\n"
)
chaine += "\n"
return chaine
def get_date_entree_etudiant(self, etudid) -> str:
"""Renvoie la date d'entree d'un étudiant: "1996" """
annees_debut = [
int(sem["annee_debut"]) for sem in self.ETUDINFO_DICT[etudid]["sems"]
]
if annees_debut:
return str(min(annees_debut))
return ""
def compute_semestres_tag(etudiants: EtudiantsJuryPE):
"""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_tools.pe_print("*** Création des semestres taggués")
formsemestres = etudiants.get_formsemestres(
semestres_recherches=pe_tools.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_tools.pe_print(f" --> Semestre taggué {nom} sur la base de {formsemestre}")
"""Créé le semestre_tag et exécute les calculs de moyennes"""
formsemestretag = pe_semestretag.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_tools.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_tools.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_tools.pe_print(" Interclassement sur la promo")
aggregats_interclasses_taggues = {}
for nom_aggregat in pe_tools.TOUS_LES_SEMESTRES + pe_tools.TOUS_LES_AGGREGATS:
pe_tools.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

View File

@ -37,22 +37,18 @@ Created on Fri Sep 9 09:15:05 2016
""" """
from app import db, log from app import db, log
from app.comp import res_sem, inscr_mod, moy_ue, moy_sem from app.comp import res_sem, moy_ue, moy_sem
from app.comp.res_common import ResultatsSemestre
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.comp.res_sem import load_formsemestre_results from app.comp.res_sem import load_formsemestre_results
from app.models import FormSemestre, Identite, DispenseUE from app.models import FormSemestre
from app.models.moduleimpls import ModuleImpl from app.models.moduleimpls import ModuleImpl
from app.pe import pe_tagtable
from app.pe import pe_tools
from app.scodoc import codes_cursus, sco_preferences
from app.scodoc import sco_tag_module from app.scodoc import sco_tag_module
from app.scodoc import sco_utils as scu
from app.scodoc.codes_cursus import UE_SPORT 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(TableTag):
class SemestreTag(pe_tagtable.TableTag):
"""Un SemestreTag représente les résultats des étudiants à un semestre, en donnant """Un SemestreTag représente les résultats des étudiants à un semestre, en donnant
accès aux moyennes par tag. accès aux moyennes par tag.
Il s'appuie principalement sur FormSemestre et sur ResultatsSemestreBUT. Il s'appuie principalement sur FormSemestre et sur ResultatsSemestreBUT.
@ -67,10 +63,8 @@ class SemestreTag(pe_tagtable.TableTag):
nom: Nom à donner au SemestreTag nom: Nom à donner au SemestreTag
formsemestre_id: Identifiant du FormSemestre sur lequel il se base formsemestre_id: Identifiant du FormSemestre sur lequel il se base
""" """
pe_tagtable.TableTag.__init__( TableTag.__init__(self, nom=nom)
self,
nom=nom
)
"""Le semestre""" """Le semestre"""
self.formsemestre_id = formsemestre_id self.formsemestre_id = formsemestre_id
self.formsemestre = FormSemestre.get_formsemestre(formsemestre_id) self.formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
@ -97,7 +91,7 @@ class SemestreTag(pe_tagtable.TableTag):
"""Les tags (en supprimant les tags réservés)""" """Les tags (en supprimant les tags réservés)"""
self.tags = get_synthese_tags_semestre(self.nt.formsemestre) self.tags = get_synthese_tags_semestre(self.nt.formsemestre)
for tag in pe_tagtable.TAGS_RESERVES: for tag in TAGS_RESERVES:
if tag in self.tags: if tag in self.tags:
del self.tags[tag] del self.tags[tag]
@ -105,7 +99,7 @@ class SemestreTag(pe_tagtable.TableTag):
self.moyennes_tags = {} self.moyennes_tags = {}
for tag in self.tags: for tag in self.tags:
pe_tools.pe_print(f" -> Traitement du tag {tag}") pe_comp.pe_print(f" -> Traitement du tag {tag}")
moy_gen_tag = self.compute_moyenne_tag(tag) moy_gen_tag = self.compute_moyenne_tag(tag)
class_gen_tag = moy_sem.comp_ranks_series(moy_gen_tag)[1] # en int class_gen_tag = moy_sem.comp_ranks_series(moy_gen_tag)[1] # en int
self.moyennes_tags[tag] = { self.moyennes_tags[tag] = {
@ -118,7 +112,7 @@ class SemestreTag(pe_tagtable.TableTag):
} }
"""Ajoute les moyennes générales de BUT pour le semestre considéré""" """Ajoute les moyennes générales de BUT pour le semestre considéré"""
pe_tools.pe_print(f" -> Traitement du tag but") pe_comp.pe_print(f" -> Traitement du tag but")
moy_gen_but = self.nt.etud_moy_gen moy_gen_but = self.nt.etud_moy_gen
class_gen_but = self.nt.etud_moy_gen_ranks_int class_gen_but = self.nt.etud_moy_gen_ranks_int
self.moyennes_tags["but"] = { self.moyennes_tags["but"] = {

View File

@ -1,128 +0,0 @@
from app.pe import pe_tagtable
from app.pe.pe_tools import PE_DEBUG, pe_print
import app.pe.pe_etudiant as pe_etudiant
from app.pe.pe_etudiant import EtudiantsJuryPE
class SetTagInterClasse(pe_tagtable.TableTag):
"""Interclasse les étudiants d'une promo (ceux diplômé) par aggrégat de même nom
(par ex: un "3S"), pour stocker leur moyenne et fournir un classement de "promo".
Les
"""
# -------------------------------------------------------------------------------------------------------------------
def __init__(self, nom:str, etudiants: EtudiantsJuryPE, aggregats_taggues: dict[str, dict]):
""""""
pe_tagtable.TableTag.__init__(self, nom)
self.etudiants = etudiants
self.aggregats_taggues = aggregats_taggues # Les moyennes par aggrégats
# -------------------------------------------------------------------------------------------
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

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
import app.pe.pe_tools as pe_tools import app.pe.pe_comp as pe_tools
from app.models import FormSemestre from app.models import FormSemestre
from app.pe.pe_etudiant import EtudiantsJuryPE, get_dernier_semestre_en_date from app.pe.pe_etudiant import EtudiantsJuryPE, get_dernier_semestre_en_date

View File

@ -38,18 +38,17 @@ Created on Fri Sep 9 09:15:05 2016
from app.comp import moy_sem from app.comp import moy_sem
from app.comp.res_sem import load_formsemestre_results from app.comp.res_sem import load_formsemestre_results
from app.models import FormSemestre from app.pe.pe_semtag import SemestreTag
from app.pe.pe_semestretag import SemestreTag from app.pe import pe_tabletags
from app.pe import pe_tagtable
import pandas as pd import pandas as pd
import numpy as np import numpy as np
from app.pe.pe_trajectoire import Trajectoire from app.pe.pe_trajectoire import Trajectoire
from app.pe.pe_etudiant import EtudiantsJuryPE from app.pe.pe_etudiant import EtudiantsJuryPE
from app.pe.pe_trajectoire import TrajectoiresJuryPE from app.pe.pe_tabletags import TableTag
class TrajectoireTag(pe_tagtable.TableTag): class TrajectoireTag(TableTag):
"""Calcule les moyennes par tag d'une combinaison de semestres """Calcule les moyennes par tag d'une combinaison de semestres
(trajectoires), identifiée par un nom d'aggrégat (par ex: '3S') et (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 par un semestre terminal, pour extraire les classements par tag pour un
@ -69,7 +68,7 @@ class TrajectoireTag(pe_tagtable.TableTag):
donnees_etudiants: EtudiantsJuryPE, donnees_etudiants: EtudiantsJuryPE,
): ):
""" """ """ """
pe_tagtable.TableTag.__init__(self, nom=nom) TableTag.__init__(self, nom=nom)
"""La trajectoire associée""" """La trajectoire associée"""
self.trajectoire_id = trajectoire.trajectoire_id self.trajectoire_id = trajectoire.trajectoire_id