# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Emmanuel Viennet emmanuel.viennet@viennet.net # ############################################################################## ############################################################################## # Module "Avis de poursuite d'étude" # conçu et développé par Cléo Baras (IUT de Grenoble) ############################################################################## """ Created on Fri Sep 9 09:15:05 2016 @author: barasc """ from app import db, log from app.comp import res_sem from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre from app.models.moduleimpls import ModuleImpl from app.pe import pe_tagtable from app.scodoc import codes_cursus from app.scodoc import sco_tag_module from app.scodoc import sco_utils as scu class SemestreTag(pe_tagtable.TableTag): """Un SemestreTag représente un tableau de notes (basé sur notesTable) modélisant les résultats des étudiants sous forme de moyennes par tag. Attributs récupérés via des NotesTables : - nt: le tableau de notes du semestre considéré - nt.inscrlist: étudiants inscrits à ce semestre, par ordre alphabétique (avec demissions) - nt.identdict: { etudid : ident } - liste des moduleimpl { ... 'module_id', ...} Attributs supplémentaires : - inscrlist/identdict: étudiants inscrits hors démissionnaires ou défaillants - _tagdict : Dictionnaire résumant les tags et les modules du semestre auxquels ils sont liés Attributs hérités de TableTag : - nom : - resultats: {tag: { etudid: (note_moy, somme_coff), ...} , ...} - rang - statistiques Redéfinition : - get_etudids() : les etudids des étudiants non défaillants ni démissionnaires """ DEBUG = True # ----------------------------------------------------------------------------- # Fonctions d'initialisation # ----------------------------------------------------------------------------- def __init__(self, notetable, sem): # Initialisation sur la base d'une notetable """Instantiation d'un objet SemestreTag à partir d'un tableau de note et des informations sur le semestre pour le dater """ 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.nt = notetable # Les attributs hérités : la liste des étudiants self.inscrlist = [ etud for etud in self.nt.inscrlist if self.nt.get_etud_etat(etud["etudid"]) == scu.INSCRIT ] self.identdict = { etudid: ident for (etudid, ident) in self.nt.identdict.items() if etudid in self.get_etudids() } # Liste des étudiants non démissionnaires et non défaillants # Les modules pris en compte dans le calcul des moyennes par tag => ceux des UE standards self.modimpls = [ modimpl for modimpl in self.nt.formsemestre.modimpls_sorted if modimpl.module.ue.type == codes_cursus.UE_STANDARD ] # la liste des modules (objet modimpl) self.somme_coeffs = sum( [ modimpl.module.coefficient for modimpl in self.modimpls if modimpl.module.coefficient is not None ] ) # ----------------------------------------------------------------------------- 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( list(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.id # liste des tags pour le modimpl concerné: tags = sco_tag_module.module_tag_list(modimpl.module.id) for ( tag ) in tags: # tag de la forme "mathématiques", "théorie", "pe:0", "maths:2" [tagname, ponderation] = sco_tag_module.split_tagname_coeff( tag ) # extrait un tagname et un éventuel coefficient de pondération (par defaut: 1) # tagname = tagname if tagname not in tagdict: # Ajout d'une clé pour le tag tagdict[tagname] = {} # Ajout du modimpl au tagname considéré tagdict[tagname][modimpl_id] = { "module_id": modimpl.module.id, # les données sur le module "coeff": modimpl.module.coefficient, # le coeff du module dans le semestre "ponderation": ponderation, # la pondération demandée pour le tag sur le module "module_code": modimpl.module.code, # le code qui doit se retrouver à l'identique dans des ue capitalisee "ue_id": modimpl.module.ue.id, # les données sur l'ue "ue_code": modimpl.module.ue.ue_code, "ue_acronyme": modimpl.module.ue.acronyme, } return tagdict # ----------------------------------------------------------------------------- def comp_MoyennesTag(self, tag, force=False) -> list: """Calcule et renvoie les "moyennes" de tous les étudiants du SemTag (non défaillants) à un tag donné, en prenant en compte tous les modimpl_id concerné par le tag, leur coeff et leur pondération. Force ou non le calcul de la moyenne lorsque des notes sont manquantes. Renvoie les informations sous la forme d'une liste [ (moy, somme_coeff_normalise, etudid), ...] """ lesMoyennes = [] for etudid in self.get_etudids(): ( notes, coeffs_norm, ponderations, ) = self.get_listesNotesEtCoeffsTagEtudiant( tag, etudid ) # les notes associées au tag coeffs = comp_coeff_pond( coeffs_norm, ponderations ) # les coeff pondérés par les tags (moyenne, somme_coeffs) = pe_tagtable.moyenne_ponderee_terme_a_terme( notes, coeffs, force=force ) lesMoyennes += [ (moyenne, somme_coeffs, etudid) ] # Un tuple (pour classement résumant les données) return lesMoyennes # ----------------------------------------------------------------------------- def get_moyennes_DUT(self): """Lit les moyennes DUT du semestre pour tous les étudiants et les renvoie au même format que comp_MoyennesTag""" return [ (self.nt.etud_moy_gen[etudid], 1.0, etudid) for etudid in self.get_etudids() ] # ----------------------------------------------------------------------------- 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(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_cap["ue_id"] for ue_cap 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 or 0.0 # le coeff (! non compatible BUT) 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 ? fids_prec = [ ue_cap["formsemestre_id"] for ue_cap in ue_capitalisees if ue_cap["ue_code"] == modimpl.module.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 # le tableau de note du semestre considéré: formsemestre_prec = FormSemestre.get_formsemestre(fid_prec) nt_prec: NotesTableCompat = res_sem.load_formsemestre_results( formsemestre_prec ) # 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 = [ modi for modi in nt_prec.formsemestre.modimpls_sorted if modi.module.code == modimpl.module.code ] if len(modimpl_prec) > 0: # si une correspondance est trouvée modprec_id = modimpl_prec[0].id moy_ue_capitalisee = get_moy_ue_from_nt(nt_prec, etudid, modprec_id) if ( moy_ue_capitalisee is None ) or 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 # nota: self.somme_coeffs peut être None coeff_norm = ( coeff / self.somme_coeffs if self.somme_coeffs else 0 ) # le coeff normalisé else: semtag_prec = SemestreTag(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) -> list[dict]: """Renvoie la liste des capitalisation effectivement capitalisées par un étudiant""" if etudid in self.nt.validations.ue_capitalisees.index: return self.nt.validations.ue_capitalisees.loc[[etudid]].to_dict("records") return [] # ----------------------------------------------------------------------------- def get_listesNotesEtCoeffsTagEtudiant(self, tag, etudid): """Renvoie un triplet (notes, coeffs_norm, ponderations) où notes, coeff_norm et ponderation désignent trois listes donnant -pour un tag donné- les note, coeff et ponderation de chaque modimpl à prendre en compte dans le calcul de la moyenne du tag. Les notes et coeff_norm sont extraits grâce à SemestreTag.get_noteEtCoeff_modimpl (donc dans semestre courant ou UE capitalisée). Les pondérations sont celles déclarées avec le tag (cf. _tagdict).""" notes = [] coeffs_norm = [] ponderations = [] for moduleimpl_id, modimpl in self.tagdict[ tag ].items(): # pour chaque module du semestre relatif au tag (note, coeff_norm) = self.get_noteEtCoeff_modimpl(moduleimpl_id, etudid) if note != None: notes.append(note) coeffs_norm.append(coeff_norm) ponderations.append(modimpl["ponderation"]) return (notes, coeffs_norm, ponderations) # ----------------------------------------------------------------------------- # Fonctions d'affichage (et d'export csv) des données du semestre en mode debug # ----------------------------------------------------------------------------- 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 + str(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) # voir avec Cléo ) + 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"]) + ") " + str(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(modimpl_id) -> dict: """Renvoie l'objet modimpl dont l'id est modimpl_id""" modimpl = db.session.get(ModuleImpl, modimpl_id) if modimpl: return modimpl if SemestreTag.DEBUG: log( "SemestreTag.get_moduleimpl( %s ) : le modimpl recherche n'existe pas" % (modimpl_id) ) return None # ********************************************** def get_moy_ue_from_nt(nt, etudid, modimpl_id) -> float: """Renvoie la moyenne de l'UE d'un etudid dans laquelle se trouve le module de modimpl_id """ # ré-écrit modimpl = get_moduleimpl(modimpl_id) # le module ue_status = nt.get_etud_ue_status(etudid, modimpl.module.ue.id) if ue_status is None: return None return ue_status["moy"]