ScoDoc-PE/pe_semestretag.py

505 lines
22 KiB
Python

# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2020 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 notes_table
import datetime
import sco_codes_parcours
import sco_tag_module
import sco_utils
import pe_tagtable
import pe_jurype
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 }
- nt._modimpls : 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
- _sum_coeff_semestre : la somme des coeffs du semestre
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
"""
DEBUG = True
# -----------------------------------------------------------------------------
# Fonctions d'initialisation
# -----------------------------------------------------------------------------
def __init__(
self, context, 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
"""
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"],
),
)
# Les attributs spécifiques
self.context = context
self.nt = notetable
# Les attributs hérités : la liste des étudiants
self.inscrlist = [etud for etud in self.nt.inscrlist if etud["etat"] == "I"]
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 modules pris en compte dans le calcul des moyennes par tag => ceux des UE standards
self.modimpls = [
modimpl
for modimpl in self.nt._modimpls
if modimpl["ue"]["type"] == sco_utils.UE_STANDARD
] # la liste des modules (objet modimpl)
# self._modimpl_ids = [modimpl['moduleimpl_id'] for modimpl in self._modimpls] # la liste de id des modules (modimpl_id)
self.somme_coeffs = sum(
[modimpl["module"]["coefficient"] for modimpl in self.modimpls]
)
# -----------------------------------------------------------------------------
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
# 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(
self.tagdict.keys() + ["dut"]
) # actualise la liste des tags
# -----------------------------------------------------------------------------
def get_etudids(self):
"""Renvoie la liste des etud_id des étudiants inscrits au semestre
"""
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.
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 = {}
for modimpl in self.modimpls:
modimpl_id = modimpl["moduleimpl_id"]
tags = sco_tag_module.module_tag_list(
self.context, modimpl["module_id"]
) # liste des tags pour le modimpl concerné
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 not tagdict.has_key(tagname): # 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["ue"]["ue_id"], # les données sur l'ue
"ue_code": modimpl["ue"]["ue_code"],
"ue_acronyme": modimpl["ue"]["acronyme"],
}
return tagdict
# -----------------------------------------------------------------------------
def comp_MoyennesTag(self, tag, force=False):
"""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.moy_gen[etudid], 1.0, etudid) for etudid in self.get_etudids()]
# -----------------------------------------------------------------------------
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.
La note et le coeff sont extraits :
1) soit des données du semestre en normalisant le coefficient par rapport à la somme des coefficients des modules du semestre,
2) soit des données des UE précédemment capitalisées, en recherchant un module de même CODE que le modimpl_id proposé,
le coefficient normalisé l'étant alors par rapport au total des coefficients du semestre auquel appartient l'ue capitalisée
"""
(note, coeff_norm) = (None, None)
modimpl = get_moduleimpl(self.nt, modimpl_id) # Le module considéré
if modimpl == None or profondeur < 0:
return (None, None)
# Y-a-t-il eu capitalisation d'UE ?
ue_capitalisees = self.get_ue_capitalisees(
etudid
) # les ue capitalisées des étudiants
ue_capitalisees_id = [
ue["ue_id"] for ue in ue_capitalisees
] # les id des ue capitalisées
# Si le module ne fait pas partie des UE capitalisées
if modimpl["module"]["ue_id"] not in ue_capitalisees_id:
note = self.nt.get_etud_mod_moy(modimpl_id, etudid) # lecture de la note
coeff = modimpl["module"]["coefficient"] # le coeff
coeff_norm = (
coeff / self.somme_coeffs if self.somme_coeffs != 0 else 0
) # le coeff normalisé
# Si le module fait partie d'une UE capitalisée
elif len(ue_capitalisees) > 0:
moy_ue_actuelle = get_moy_ue_from_nt(
self.nt, etudid, modimpl_id
) # la moyenne actuelle
# A quel semestre correspond l'ue capitalisée et quelles sont ses notes ?
# fid_prec = [ ue['formsemestre_id'] for ue in ue_capitalisees if ue['ue_id'] == modimpl['module']['ue_id'] ][0]
# semestre_id = modimpl['module']['semestre_id']
fids_prec = [
ue["formsemestre_id"]
for ue in ue_capitalisees
if ue["ue_code"] == modimpl["ue"]["ue_code"]
] # and ue['semestre_id'] == semestre_id]
if len(fids_prec) > 0:
# => le formsemestre_id du semestre dont vient la capitalisation
fid_prec = fids_prec[0]
# Lecture des notes de ce semestre
nt_prec = self.context.Notes._getNotesCache().get_NotesTable(
self.context.Notes, fid_prec
) # le tableau de note du semestre considéré
# Y-a-t-il un module équivalent c'est à dire correspondant au même code (le module_id n'étant pas significatif en cas de changement de PPN)
modimpl_prec = [
module
for module in nt_prec._modimpls
if module["module"]["code"] == modimpl["module"]["code"]
]
if len(modimpl_prec) > 0: # si une correspondance est trouvée
modprec_id = modimpl_prec[0]["moduleimpl_id"]
moy_ue_capitalisee = get_moy_ue_from_nt(nt_prec, etudid, modprec_id)
if (
moy_ue_actuelle >= moy_ue_capitalisee
): # on prend la meilleure ue
note = self.nt.get_etud_mod_moy(
modimpl_id, etudid
) # lecture de la note
coeff = modimpl["module"]["coefficient"] # le coeff
coeff_norm = (
coeff / self.somme_coeffs if self.somme_coeffs != 0 else 0
) # le coeff normalisé
else:
semtag_prec = SemestreTag(self.context, nt_prec, nt_prec.sem)
(note, coeff_norm) = semtag_prec.get_noteEtCoeff_modimpl(
modprec_id, etudid, profondeur=profondeur - 1
) # lecture de la note via le semtag associé au modimpl capitalisé
# Sinon - pas de notes à prendre en compte
return (note, coeff_norm)
# -----------------------------------------------------------------------------
def get_ue_capitalisees(self, etudid):
"""Renvoie la liste des ue_id effectivement capitalisées par un étudiant"""
# return [ ue for ue in self.nt.ue_capitalisees[etudid] if self.nt.get_etud_ue_status(etudid,ue['ue_id'])['is_capitalized'] ]
return self.nt.ue_capitalisees[etudid]
# -----------------------------------------------------------------------------
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
# -----------------------------------------------------------------------------
def str_detail_resultat_d_un_tag(self, tag, etudid=None, delim=";"):
"""Renvoie une chaine de caractère décrivant les résultats d'étudiants à un tag :
rappelle les notes obtenues dans les modules à prendre en compte, les moyennes et les rangs calculés.
Si etudid=None, tous les étudiants inscrits dans le semestre sont pris en compte. Sinon seuls les étudiants indiqués sont affichés."""
# Entete
chaine = delim.join(["%15s" % "nom", "etudid"]) + delim
taglist = self.get_all_tags()
if tag in taglist:
for mod in self.tagdict[tag].values():
chaine += mod["module_code"] + delim
chaine += ("%1.1f" % mod["ponderation"]) + delim
chaine += "coeff" + delim
chaine += delim.join(
["moyenne", "rang", "nbinscrit", "somme_coeff", "somme_coeff"]
) # ligne 1
chaine += "\n"
# Différents cas de boucles sur les étudiants (de 1 à plusieurs)
if etudid == None:
lesEtuds = self.get_etudids()
elif isinstance(etudid, str) and etudid in self.get_etudids():
lesEtuds = [etudid]
elif isinstance(etudid, list):
lesEtuds = [eid for eid in self.get_etudids() if eid in etudid]
else:
lesEtuds = []
for etudid in lesEtuds:
descr = "%15s" % self.nt.get_nom_short(etudid)[:15] + delim + etudid + delim
if tag in taglist:
for modimpl_id in self.tagdict[tag]:
(note, coeff) = self.get_noteEtCoeff_modimpl(modimpl_id, etudid)
descr += (
(
"%2.2f" % note
if note != None and isinstance(note, float)
else str(note)
)
+ delim
+ (
"%1.5f" % coeff
if coeff != None and isinstance(coeff, float)
else str(coeff)
)
+ delim
+ (
"%1.5f" % (coeff * self.somme_coeffs)
if coeff != None and isinstance(coeff, float)
else str(coeff * self._sum_coeff_semestre)
)
+ delim
)
moy = self.get_moy_from_resultats(tag, etudid)
rang = self.get_rang_from_resultats(tag, etudid)
coeff = self.get_coeff_from_resultats(tag, etudid)
tot = (
coeff * self.somme_coeffs
if coeff != None
and self.somme_coeffs != None
and isinstance(coeff, float)
else None
)
descr += (
pe_tagtable.TableTag.str_moytag(
moy, rang, len(self.get_etudids()), delim=delim
)
+ delim
)
descr += (
(
"%1.5f" % coeff
if coeff != None and isinstance(coeff, float)
else str(coeff)
)
+ delim
+ (
"%.2f" % (tot)
if tot != None
else str(coeff) + "*" + str(self.somme_coeffs)
)
)
chaine += descr
chaine += "\n"
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"""
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()
for tag in taglist:
chaine += " > " + tag + ": "
for (modid, mod) in self.tagdict[tag].items():
chaine += (
mod["module_code"]
+ " ("
+ str(mod["coeff"])
+ "*"
+ str(mod["ponderation"])
+ ") "
+ modid
+ ", "
)
chaine += "\n"
return chaine
# ************************************************************************
# Fonctions diverses
# ************************************************************************
# *********************************************
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))
]
# -----------------------------------------------------------------------------
def get_moduleimpl(nt, modimpl_id):
"""Renvoie l'objet modimpl dont l'id est modimpl_id fourni dans la note table nt,
en utilisant l'attribut nt._modimpls"""
modimplids = [
modimpl["moduleimpl_id"] for modimpl in nt._modimpls
] # la liste de id des modules (modimpl_id)
if modimpl_id not in modimplids:
if SemestreTag.DEBUG:
print "SemestreTag.get_moduleimpl( %s ) : le modimpl recherche n'existe pas" % (
modimpl_id
)
return None
return nt._modimpls[modimplids.index(modimpl_id)]
# **********************************************
def get_moy_ue_from_nt(nt, etudid, modimpl_id):
"""Renvoie la moyenne de l'UE d'un etudid dans laquelle se trouve le module de modimpl_id
en partant du note table nt"""
mod = get_moduleimpl(nt, modimpl_id) # le module
indice = 0
while indice < len(nt._ues):
if (
nt._ues[indice]["ue_id"] == mod["module"]["ue_id"]
): # si les ue_id correspond
data = [
ligne for ligne in nt.T if ligne[-1] == etudid
] # les notes de l'étudiant
if data:
return data[0][indice + 1] # la moyenne à l'ue
else:
indice += 1
return None # si non trouvé