# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2022 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 # ############################################################################## """Calculs sur les notes et cache des résultats Ancien code ScoDoc 7 en cours de rénovation """ from operator import itemgetter from flask import g, url_for from app.but import bulletin_but from app.models import FormSemestre, Identite from app.models import ScoDocSiteConfig import app.scodoc.sco_utils as scu from app.scodoc.sco_utils import ModuleType import app.scodoc.notesdb as ndb from app import log from app.scodoc.sco_formulas import NoteVector from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_formsemestre import ( formsemestre_uecoef_list, formsemestre_uecoef_create, ) from app.scodoc.sco_codes_parcours import ( DEF, UE_SPORT, UE_is_fondamentale, UE_is_professionnelle, ) from app.scodoc.sco_parcours_dut import formsemestre_get_etud_capitalisation from app.scodoc import sco_codes_parcours from app.scodoc import sco_compute_moy from app.scodoc import sco_cache from app.scodoc import sco_edit_matiere from app.scodoc import sco_edit_module from app.scodoc import sco_edit_ue from app.scodoc import sco_evaluations from app.scodoc import sco_formations from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_groups from app.scodoc import sco_moduleimpl from app.scodoc import sco_parcours_dut from app.scodoc import sco_preferences from app.scodoc import sco_etud def comp_ranks(T): """Calcul rangs à partir d'une liste ordonnée de tuples [ (valeur, ..., etudid) ] (valeur est une note numérique), en tenant compte des ex-aequos Le resultat est: { etudid : rang } où rang est une chaine decrivant le rang """ rangs = {} # { etudid : rang } (rang est une chaine) nb_ex = 0 # nb d'ex-aequo consécutifs en cours for i in range(len(T)): # test ex-aequo if i < len(T) - 1: next = T[i + 1][0] else: next = None moy = T[i][0] if nb_ex: srang = "%d ex" % (i + 1 - nb_ex) if moy == next: nb_ex += 1 else: nb_ex = 0 else: if moy == next: srang = "%d ex" % (i + 1 - nb_ex) nb_ex = 1 else: srang = "%d" % (i + 1) rangs[T[i][-1]] = srang # str(i+1) return rangs def get_sem_ues_modimpls(formsemestre_id, modimpls=None): """Get liste des UE du semestre (à partir des moduleimpls) (utilisé quand on ne peut pas construire nt et faire nt.get_ues_stat_dict()) """ if modimpls is None: modimpls = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id) uedict = {} for modimpl in modimpls: mod = sco_edit_module.module_list(args={"module_id": modimpl["module_id"]})[0] modimpl["module"] = mod if not mod["ue_id"] in uedict: ue = sco_edit_ue.ue_list(args={"ue_id": mod["ue_id"]})[0] uedict[ue["ue_id"]] = ue ues = list(uedict.values()) ues.sort(key=lambda u: u["numero"]) return ues, modimpls def comp_etud_sum_coef_modules_ue(formsemestre_id, etudid, ue_id): """Somme des coefficients des modules de l'UE dans lesquels cet étudiant est inscrit ou None s'il n'y a aucun module. (nécessaire pour éviter appels récursifs de nt, qui peuvent boucler) """ infos = ndb.SimpleDictFetch( """SELECT mod.coefficient FROM notes_modules mod, notes_moduleimpl mi, notes_moduleimpl_inscription ins WHERE mod.id = mi.module_id and ins.etudid = %(etudid)s and ins.moduleimpl_id = mi.id and mi.formsemestre_id = %(formsemestre_id)s and mod.ue_id = %(ue_id)s """, {"etudid": etudid, "formsemestre_id": formsemestre_id, "ue_id": ue_id}, ) if not infos: return None else: s = sum(x["coefficient"] for x in infos) return s class NotesTable: """Une NotesTable représente un tableau de notes pour un semestre de formation. Les colonnes sont des modules. Les lignes des étudiants. On peut calculer les moyennes par étudiant (pondérées par les coefs) ou les moyennes par module. Attributs publics (en lecture): - inscrlist: étudiants inscrits à ce semestre, par ordre alphabétique (avec demissions) - identdict: { etudid : ident } - sem : le formsemestre get_table_moyennes_triees: [ (moy_gen, moy_ue1, moy_ue2, ... moy_ues, moy_mod1, ..., moy_modn, etudid) ] (où toutes les valeurs sont soit des nombres soit des chaines spéciales comme 'NA', 'NI'), incluant les UE de sport - bonus[etudid] : valeur du bonus "sport". Attributs privés: - _modmoys : { moduleimpl_id : { etudid: note_moyenne_dans_ce_module } } - _ues : liste des UE de ce semestre (hors capitalisees) - _matmoys : { matiere_id : { etudid: note moyenne dans cette matiere } } """ def __init__(self, formsemestre_id): # log(f"NotesTable( formsemestre_id={formsemestre_id} )") raise NotImplementedError() # XXX if not formsemestre_id: raise ValueError("invalid formsemestre_id (%s)" % formsemestre_id) self.formsemestre_id = formsemestre_id cnx = ndb.GetDBConnexion() self.sem = sco_formsemestre.get_formsemestre(formsemestre_id) self.moduleimpl_stats = {} # { moduleimpl_id : {stats} } self._uecoef = {} # { ue_id : coef } cache coef manuels ue cap self._evaluations_etats = None # liste des evaluations avec état self.use_ue_coefs = sco_preferences.get_preference( "use_ue_coefs", formsemestre_id ) # si vrai, bloque calcul des moy gen. et d'UE.: self.block_moyennes = self.sem["block_moyennes"] # Infos sur les etudiants self.inscrlist = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( args={"formsemestre_id": formsemestre_id} ) # infos identite etudiant # xxx sous-optimal: 1/select par etudiant -> 0.17" pour identdict sur GTR1 ! self.identdict = {} # { etudid : ident } self.inscrdict = {} # { etudid : inscription } for x in self.inscrlist: i = sco_etud.etudident_list(cnx, {"etudid": x["etudid"]})[0] self.identdict[x["etudid"]] = i self.inscrdict[x["etudid"]] = x x["nomp"] = (i["nom_usuel"] or i["nom"]) + i["prenom"] # pour tri # Tri les etudids par NOM self.inscrlist.sort(key=itemgetter("nomp")) # { etudid : rang dans l'ordre alphabetique } self._rang_alpha = {e["etudid"]: i for i, e in enumerate(self.inscrlist)} self.bonus = scu.DictDefault(defaultvalue=0) # Notes dans les modules { moduleimpl_id : { etudid: note_moyenne_dans_ce_module } } ( self._modmoys, self._modimpls, self._valid_evals_per_mod, valid_evals, mods_att, self.expr_diagnostics, ) = sco_compute_moy.formsemestre_compute_modimpls_moyennes( self, formsemestre_id ) self._mods_att = mods_att # liste des modules avec des notes en attente self._matmoys = {} # moyennes par matieres self._valid_evals = {} # { evaluation_id : eval } for e in valid_evals: self._valid_evals[e["evaluation_id"]] = e # Liste des modules et UE uedict = {} # public member: { ue_id : ue } self.uedict = uedict # les ues qui ont un modimpl dans ce semestre for modimpl in self._modimpls: # module has been added by formsemestre_compute_modimpls_moyennes mod = modimpl["module"] if not mod["ue_id"] in uedict: ue = sco_edit_ue.ue_list(args={"ue_id": mod["ue_id"]})[0] uedict[ue["ue_id"]] = ue else: ue = uedict[mod["ue_id"]] modimpl["ue"] = ue # add ue dict to moduleimpl self._matmoys[mod["matiere_id"]] = {} mat = sco_edit_matiere.matiere_list(args={"matiere_id": mod["matiere_id"]})[ 0 ] modimpl["mat"] = mat # add matiere dict to moduleimpl # calcul moyennes du module et stocke dans le module # nb_inscrits, nb_notes, nb_abs, nb_neutre, moy, median, last_modif= self.formation = sco_formations.formation_list( args={"formation_id": self.sem["formation_id"]} )[0] self.parcours = sco_codes_parcours.get_parcours_from_code( self.formation["type_parcours"] ) # En APC, il faut avoir toutes les UE du semestre # (elles n'ont pas nécessairement un module rattaché): if self.parcours.APC_SAE: formsemestre = FormSemestre.query.get(formsemestre_id) for ue in formsemestre.query_ues(): if ue.id not in self.uedict: self.uedict[ue.id] = ue.to_dict() # Decisions jury et UE capitalisées self.comp_decisions_jury() self.comp_ue_capitalisees() # Liste des moyennes de tous, en chaines de car., triées self._ues = list(uedict.values()) self._ues.sort(key=lambda u: u["numero"]) T = [] self.moy_gen = {} # etudid : moy gen (avec UE capitalisées) self.moy_ue = {} # ue_id : { etudid : moy ue } (valeur numerique) self.etud_moy_infos = {} # etudid : resultats de comp_etud_moy_gen() valid_moy = [] # liste des valeurs valides de moyenne generale (pour min/max) for ue in self._ues: self.moy_ue[ue["ue_id"]] = {} self._etud_moy_ues = {} # { etudid : { ue_id : {'moy', 'sum_coefs', ... } } for etudid in self.get_etudids(): etud_moy_gen = self.comp_etud_moy_gen(etudid, cnx) self.etud_moy_infos[etudid] = etud_moy_gen ue_status = etud_moy_gen["moy_ues"] self._etud_moy_ues[etudid] = ue_status moy_gen = etud_moy_gen["moy"] self.moy_gen[etudid] = moy_gen if etud_moy_gen["sum_coefs"] > 0: valid_moy.append(moy_gen) moy_ues = [] for ue in self._ues: moy_ue = ue_status[ue["ue_id"]]["moy"] moy_ues.append(moy_ue) self.moy_ue[ue["ue_id"]][etudid] = moy_ue t = [moy_gen] + moy_ues # is_cap = {} # ue_id : is_capitalized for ue in self._ues: is_cap[ue["ue_id"]] = ue_status[ue["ue_id"]]["is_capitalized"] for modimpl in self.get_modimpls_dict(): val = self.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid) if is_cap[modimpl["module"]["ue_id"]]: t.append("-c-") else: t.append(val) # t.append(etudid) T.append(t) self.T = T # tri par moyennes décroissantes, # en laissant les demissionnaires a la fin, par ordre alphabetique self.T.sort(key=self._row_key) if len(valid_moy): self.moy_min = min(valid_moy) self.moy_max = max(valid_moy) else: self.moy_min = self.moy_max = "NA" # calcul rangs (/ moyenne generale) self.etud_moy_gen_ranks = comp_ranks(T) self.rangs_groupes = ( {} ) # { group_id : { etudid : rang } } (lazy, see get_etud_rang_group) self.group_etuds = ( {} ) # { group_id : set of etudids } (lazy, see get_etud_rang_group) # calcul rangs dans chaque UE ue_rangs = ( {} ) # ue_rangs[ue_id] = ({ etudid : rang }, nb_inscrits) (rang est une chaine) for ue in self._ues: ue_id = ue["ue_id"] val_ids = [ (self.moy_ue[ue_id][etudid], etudid) for etudid in self.moy_ue[ue_id] ] ue_eff = len( [x for x in val_ids if isinstance(x[0], float)] ) # nombre d'étudiants avec une note dans l'UE val_ids.sort(key=self._row_key) ue_rangs[ue_id] = ( comp_ranks(val_ids), ue_eff, ) # et non: len(self.moy_ue[ue_id]) qui est l'effectif de la promo self.ue_rangs = ue_rangs # ---- calcul rangs dans les modules self.mod_rangs = {} for modimpl in self._modimpls: vals = self._modmoys[modimpl["moduleimpl_id"]] val_ids = [(vals[etudid], etudid) for etudid in vals.keys()] val_ids.sort(key=self._row_key) self.mod_rangs[modimpl["moduleimpl_id"]] = (comp_ranks(val_ids), len(vals)) # self.compute_moy_moy() # log(f"NotesTable( formsemestre_id={formsemestre_id} ) done.") def _row_key(self, x): """clé de tri par moyennes décroissantes, en laissant les demissionnaires a la fin, par ordre alphabetique. (moy_gen, rang_alpha) """ try: moy = -float(x[0]) except (ValueError, TypeError): moy = 1000.0 return (moy, self._rang_alpha[x[-1]]) def get_etudids(self, sorted=False): if sorted: # Tri par moy. generale décroissante return [x[-1] for x in self.T] else: # Tri par ordre alphabetique de NOM return [x["etudid"] for x in self.inscrlist] def get_sexnom(self, etudid): "M. DUPONT" etud = self.identdict[etudid] return etud["civilite_str"] + " " + (etud["nom_usuel"] or etud["nom"]).upper() def get_nom_short(self, etudid): "formatte nom d'un etud (pour table recap)" etud = self.identdict[etudid] # Attention aux caracteres multibytes pour decouper les 2 premiers: return ( (etud["nom_usuel"] or etud["nom"]).upper() + " " + etud["prenom"].capitalize()[:2] + "." ) def get_nom_long(self, etudid): "formatte nom d'un etud: M. Pierre DUPONT" etud = self.identdict[etudid] return sco_etud.format_nomprenom(etud) def get_displayed_etud_code(self, etudid): 'code à afficher sur les listings "anonymes"' return self.identdict[etudid]["code_nip"] or self.identdict[etudid]["etudid"] def get_etud_etat(self, etudid): "Etat de l'etudiant: 'I', 'D', DEF ou '' (si pas connu dans ce semestre)" if etudid in self.inscrdict: return self.inscrdict[etudid]["etat"] else: return "" def get_etud_etat_html(self, etudid): etat = self.inscrdict[etudid]["etat"] if etat == "I": return "" elif etat == "D": return ' <font color="red">(DEMISSIONNAIRE)</font> ' elif etat == DEF: return ' <font color="red">(DEFAILLANT)</font> ' else: return ' <font color="red">(%s)</font> ' % etat def get_ues_stat_dict(self, filter_sport=False): # was get_ues() """Liste des UEs, ordonnée par numero. Si filter_sport, retire les UE de type SPORT """ if not filter_sport: return self._ues else: return [ue for ue in self._ues if ue["type"] != UE_SPORT] def get_modimpls_dict(self, ue_id=None): "Liste des modules pour une UE (ou toutes si ue_id==None), triés par matières." if ue_id is None: r = self._modimpls else: r = [m for m in self._modimpls if m["ue"]["ue_id"] == ue_id] # trie la liste par ue.numero puis mat.numero puis mod.numero r.sort( key=lambda x: (x["ue"]["numero"], x["mat"]["numero"], x["module"]["numero"]) ) return r def get_etud_eval_note(self, etudid, evaluation_id): "note d'un etudiant a une evaluation" return self._valid_evals[evaluation_id]["notes"][etudid] def get_evals_in_mod(self, moduleimpl_id): "liste des evaluations valides dans un module" return [ e for e in self._valid_evals.values() if e["moduleimpl_id"] == moduleimpl_id ] def get_mod_stats(self, moduleimpl_id): """moyenne generale, min, max pour un module Ne prend en compte que les evaluations où toutes les notes sont entrées Cache le resultat. """ if moduleimpl_id in self.moduleimpl_stats: return self.moduleimpl_stats[moduleimpl_id] nb_notes = 0 sum_notes = 0.0 nb_missing = 0 moys = self._modmoys[moduleimpl_id] vals = [] for etudid in self.get_etudids(): # saute les demissionnaires et les défaillants: if self.inscrdict[etudid]["etat"] != "I": continue val = moys.get(etudid, None) # None si non inscrit try: vals.append(float(val)) except: nb_missing = nb_missing + 1 sum_notes = sum(vals) nb_notes = len(vals) if nb_notes > 0: moy = sum_notes / nb_notes max_note, min_note = max(vals), min(vals) else: moy, min_note, max_note = "NA", "-", "-" s = { "moy": moy, "max": max_note, "min": min_note, "nb_notes": nb_notes, "nb_missing": nb_missing, "nb_valid_evals": len(self._valid_evals_per_mod[moduleimpl_id]), } self.moduleimpl_stats[moduleimpl_id] = s return s def compute_moy_moy(self): """precalcule les moyennes d'UE et generale (moyennes sur tous les etudiants), et les stocke dans self.moy_moy, self.ue['moy'] Les moyennes d'UE ne tiennent pas compte des capitalisations. """ ues = self.get_ues_stat_dict() sum_moy = 0 # la somme des moyennes générales valides nb_moy = 0 # le nombre de moyennes générales valides for ue in ues: ue["_notes"] = [] # liste tmp des valeurs de notes valides dans l'ue nb_dem = 0 # nb d'étudiants démissionnaires dans le semestre nb_def = 0 # nb d'étudiants défaillants dans le semestre T = self.get_table_moyennes_triees() for t in T: etudid = t[-1] # saute les demissionnaires et les défaillants: if self.inscrdict[etudid]["etat"] != "I": if self.inscrdict[etudid]["etat"] == "D": nb_dem += 1 if self.inscrdict[etudid]["etat"] == DEF: nb_def += 1 continue try: sum_moy += float(t[0]) nb_moy += 1 except: pass i = 0 for ue in ues: i += 1 try: ue["_notes"].append(float(t[i])) except: pass self.nb_demissions = nb_dem self.nb_defaillants = nb_def if nb_moy > 0: self.moy_moy = sum_moy / nb_moy else: self.moy_moy = "-" i = 0 for ue in ues: i += 1 ue["nb_vals"] = len(ue["_notes"]) if ue["nb_vals"] > 0: ue["moy"] = sum(ue["_notes"]) / ue["nb_vals"] ue["max"] = max(ue["_notes"]) ue["min"] = min(ue["_notes"]) else: ue["moy"], ue["max"], ue["min"] = "", "", "" del ue["_notes"] def get_etud_mod_moy(self, moduleimpl_id, etudid): """moyenne d'un etudiant dans un module (ou NI si non inscrit)""" return self._modmoys[moduleimpl_id].get(etudid, "NI") def get_etud_mat_moy(self, matiere_id, etudid): """moyenne d'un étudiant dans une matière (ou NA si pas de notes)""" matmoy = self._matmoys.get(matiere_id, None) if not matmoy: return "NM" # non inscrit # log('*** oups: get_etud_mat_moy(%s, %s)' % (matiere_id, etudid)) # raise ValueError('matiere invalide !') # should not occur return matmoy.get(etudid, "NA") def comp_etud_moy_ue(self, etudid, ue_id=None, cnx=None): """Calcule moyenne gen. pour un etudiant dans une UE Ne prend en compte que les evaluations où toutes les notes sont entrées Return a dict(moy, nb_notes, nb_missing, sum_coefs) Si pas de notes, moy == 'NA' et sum_coefs==0 Si non inscrit, moy == 'NI' et sum_coefs==0 """ assert ue_id modimpls = self.get_modimpls_dict(ue_id) nb_notes = 0 # dans cette UE sum_notes = 0.0 sum_coefs = 0.0 nb_missing = 0 # nb de modules sans note dans cette UE notes_bonus_gen = [] # liste des notes de sport et culture coefs_bonus_gen = [] ue_malus = 0.0 # malus à appliquer à cette moyenne d'UE notes = NoteVector() coefs = NoteVector() coefs_mask = NoteVector() # 0/1, 0 si coef a ete annulé matiere_id_last = None matiere_sum_notes = matiere_sum_coefs = 0.0 est_inscrit = False # inscrit à l'un des modules de cette UE ? for modimpl in modimpls: # module ne faisant pas partie d'une UE capitalisee val = self._modmoys[modimpl["moduleimpl_id"]].get(etudid, "NI") # si 'NI', etudiant non inscrit a ce module if val != "NI": est_inscrit = True if modimpl["module"]["module_type"] == ModuleType.STANDARD: coef = modimpl["module"]["coefficient"] if modimpl["ue"]["type"] != UE_SPORT: notes.append(val, name=modimpl["module"]["code"]) try: sum_notes += val * coef sum_coefs += coef nb_notes = nb_notes + 1 coefs.append(coef) coefs_mask.append(1) matiere_id = modimpl["module"]["matiere_id"] if ( matiere_id_last and matiere_id != matiere_id_last and matiere_sum_coefs ): self._matmoys[matiere_id_last][etudid] = ( matiere_sum_notes / matiere_sum_coefs ) matiere_sum_notes = matiere_sum_coefs = 0.0 matiere_sum_notes += val * coef matiere_sum_coefs += coef matiere_id_last = matiere_id except TypeError: # val == "NI" "NA" assert val == "NI" or val == "NA" or val == "ERR" nb_missing = nb_missing + 1 coefs.append(0) coefs_mask.append(0) else: # UE_SPORT: # la note du module de sport agit directement sur la moyenne gen. try: notes_bonus_gen.append(float(val)) coefs_bonus_gen.append(coef) except: # log('comp_etud_moy_ue: exception: val=%s coef=%s' % (val,coef)) pass elif modimpl["module"]["module_type"] == ModuleType.MALUS: try: ue_malus += val except: pass # si non inscrit ou manquant, ignore elif modimpl["module"]["module_type"] in ( ModuleType.RESSOURCE, ModuleType.SAE, ): # XXX temporaire pour ne pas bloquer durant le dev pass else: raise ValueError( "invalid module type (%s)" % modimpl["module"]["module_type"] ) if matiere_id_last and matiere_sum_coefs: self._matmoys[matiere_id_last][etudid] = ( matiere_sum_notes / matiere_sum_coefs ) # Calcul moyenne: if sum_coefs > 0: moy = sum_notes / sum_coefs if ue_malus: moy -= ue_malus moy = max(scu.NOTES_MIN, min(moy, 20.0)) moy_valid = True else: moy = "NA" moy_valid = False # Recalcule la moyenne en utilisant une formule utilisateur expr_diag = {} formula = sco_compute_moy.get_ue_expression(self.formsemestre_id, ue_id) if formula: moy = sco_compute_moy.compute_user_formula( self.sem, etudid, moy, moy_valid, notes, coefs, coefs_mask, formula, diag_info=expr_diag, ) if expr_diag: expr_diag["ue_id"] = ue_id self.expr_diagnostics.append(expr_diag) return dict( moy=moy, nb_notes=nb_notes, nb_missing=nb_missing, sum_coefs=sum_coefs, notes_bonus_gen=notes_bonus_gen, coefs_bonus_gen=coefs_bonus_gen, expr_diag=expr_diag, ue_malus=ue_malus, est_inscrit=est_inscrit, ) def comp_etud_moy_gen(self, etudid, cnx): """Calcule moyenne gen. pour un etudiant Return a dict: moy : moyenne générale nb_notes, nb_missing, sum_coefs ects_pot : (float) nb de crédits ECTS qui seraient validés (sous réserve de validation par le jury), ects_pot_fond: (float) nb d'ECTS issus d'UE fondamentales (non électives) ects_pot_pro: (float) nb d'ECTS issus d'UE pro moy_ues : { ue_id : ue_status } où ue_status = { 'est_inscrit' : True si étudiant inscrit à au moins un module de cette UE 'moy' : moyenne, avec capitalisation eventuelle 'capitalized_ue_id' : id de l'UE capitalisée 'coef_ue' : coef de l'UE utilisé pour le calcul de la moyenne générale (la somme des coefs des modules, ou le coef d'UE capitalisée, ou encore le coef d'UE si l'option use_ue_coefs est active) 'cur_moy_ue' : moyenne de l'UE en cours (sans considérer de capitalisation) 'cur_coef_ue': coefficient de l'UE courante (inutilisé ?) 'is_capitalized' : True|False, 'ects_pot' : (float) nb de crédits ECTS qui seraient validés (sous réserve de validation par le jury), 'ects_pot_fond': 0. si UE non fondamentale, = ects_pot sinon, 'ects_pot_pro' : 0 si UE non pro, = ects_pot sinon, 'formsemestre_id' : (si capitalisee), 'event_date' : (si capitalisee) } Si pas de notes, moy == 'NA' et sum_coefs==0 Prend toujours en compte les UE capitalisées. """ # Si l'étudiant a Démissionné ou est DEFaillant, on n'enregistre pas ses moyennes block_computation = ( self.inscrdict[etudid]["etat"] == "D" or self.inscrdict[etudid]["etat"] == DEF or self.block_moyennes ) moy_ues = {} notes_bonus_gen = ( [] ) # liste des notes de sport et culture (s'appliquant à la MG) coefs_bonus_gen = [] nb_notes = 0 # nb de notes d'UE (non capitalisees) sum_notes = 0.0 # somme des notes d'UE # somme des coefs d'UE (eux-même somme des coefs de modules avec notes): sum_coefs = 0.0 nb_missing = 0 # nombre d'UE sans notes sem_ects_pot = 0.0 sem_ects_pot_fond = 0.0 sem_ects_pot_pro = 0.0 for ue in self.get_ues_stat_dict(): # - On calcule la moyenne d'UE courante: if not block_computation: mu = self.comp_etud_moy_ue(etudid, ue_id=ue["ue_id"], cnx=cnx) else: mu = dict( moy="NA", nb_notes=0, nb_missing=0, sum_coefs=0, notes_bonus_gen=0, coefs_bonus_gen=0, expr_diag="", est_inscrit=False, ) # infos supplementaires pouvant servir au calcul du bonus sport mu["ue"] = ue moy_ues[ue["ue_id"]] = mu # - Faut-il prendre une UE capitalisée ? if mu["moy"] != "NA" and mu["est_inscrit"]: max_moy_ue = mu["moy"] else: # pas de notes dans l'UE courante, ou pas inscrit max_moy_ue = 0.0 if not mu["est_inscrit"]: coef_ue = 0.0 else: if self.use_ue_coefs: coef_ue = mu["ue"]["coefficient"] else: # coef UE = sum des coefs modules coef_ue = mu["sum_coefs"] # is_capitalized si l'UE prise en compte est une UE capitalisée mu["is_capitalized"] = False # was_capitalized s'il y a precedemment une UE capitalisée (pas forcement meilleure) mu["was_capitalized"] = False is_external = False event_date = None if not block_computation: for ue_cap in self.ue_capitalisees[etudid]: if ue_cap["ue_code"] == ue["ue_code"]: moy_ue_cap = ue_cap["moy"] mu["was_capitalized"] = True event_date = event_date or ue_cap["event_date"] if ( (moy_ue_cap != "NA") and isinstance(moy_ue_cap, float) and isinstance(max_moy_ue, float) and (moy_ue_cap > max_moy_ue) ): # meilleure UE capitalisée event_date = ue_cap["event_date"] max_moy_ue = moy_ue_cap mu["is_capitalized"] = True capitalized_ue_id = ue_cap["ue_id"] formsemestre_id = ue_cap["formsemestre_id"] coef_ue = self.get_etud_ue_cap_coef( etudid, ue, ue_cap, cnx=cnx ) is_external = ue_cap["is_external"] mu["cur_moy_ue"] = mu["moy"] # la moyenne dans le sem. courant if mu["est_inscrit"]: mu["cur_coef_ue"] = mu["sum_coefs"] else: mu["cur_coef_ue"] = 0.0 mu["moy"] = max_moy_ue # la moyenne d'UE a prendre en compte mu["is_external"] = is_external # validation externe (dite "antérieure") mu["coef_ue"] = coef_ue # coef reel ou coef de l'ue si capitalisee if mu["is_capitalized"]: mu["formsemestre_id"] = formsemestre_id mu["capitalized_ue_id"] = capitalized_ue_id if mu["was_capitalized"]: mu["event_date"] = event_date # - ECTS ? ("pot" pour "potentiels" car les ECTS ne seront acquises qu'apres validation du jury if ( isinstance(mu["moy"], float) and mu["moy"] >= self.parcours.NOTES_BARRE_VALID_UE ): mu["ects_pot"] = ue["ects"] or 0.0 if UE_is_fondamentale(ue["type"]): mu["ects_pot_fond"] = mu["ects_pot"] else: mu["ects_pot_fond"] = 0.0 if UE_is_professionnelle(ue["type"]): mu["ects_pot_pro"] = mu["ects_pot"] else: mu["ects_pot_pro"] = 0.0 else: mu["ects_pot"] = 0.0 mu["ects_pot_fond"] = 0.0 mu["ects_pot_pro"] = 0.0 sem_ects_pot += mu["ects_pot"] sem_ects_pot_fond += mu["ects_pot_fond"] sem_ects_pot_pro += mu["ects_pot_pro"] # - Calcul moyenne générale dans le semestre: if mu["is_capitalized"]: try: sum_notes += mu["moy"] * mu["coef_ue"] sum_coefs += mu["coef_ue"] except: # pas de note dans cette UE pass else: if mu["coefs_bonus_gen"]: notes_bonus_gen.extend(mu["notes_bonus_gen"]) coefs_bonus_gen.extend(mu["coefs_bonus_gen"]) # try: sum_notes += mu["moy"] * mu["sum_coefs"] sum_coefs += mu["sum_coefs"] nb_notes = nb_notes + 1 except TypeError: nb_missing = nb_missing + 1 # Le resultat: infos = dict( nb_notes=nb_notes, nb_missing=nb_missing, sum_coefs=sum_coefs, moy_ues=moy_ues, ects_pot=sem_ects_pot, ects_pot_fond=sem_ects_pot_fond, ects_pot_pro=sem_ects_pot_pro, sem=self.sem, ) # ---- Calcul moyenne (avec bonus sport&culture) if sum_coefs <= 0 or block_computation: infos["moy"] = "NA" else: if self.use_ue_coefs: # Calcul optionnel (mai 2020) # moyenne pondére par leurs coefficients des moyennes d'UE sum_moy_ue = 0 sum_coefs_ue = 0 for mu in moy_ues.values(): # mu["moy"] can be a number, or "NA", or "ERR" (user-defined UE formulas) if ( (mu["ue"]["type"] != UE_SPORT) and scu.isnumber(mu["moy"]) and (mu["est_inscrit"] or mu["is_capitalized"]) ): coef_ue = mu["ue"]["coefficient"] sum_moy_ue += mu["moy"] * coef_ue sum_coefs_ue += coef_ue if sum_coefs_ue != 0: infos["moy"] = sum_moy_ue / sum_coefs_ue else: infos["moy"] = "NA" else: # Calcul standard ScoDoc: moyenne pondérée des notes de modules infos["moy"] = sum_notes / sum_coefs if notes_bonus_gen and infos["moy"] != "NA": # regle de calcul maison (configurable, voir bonus_sport.py) if sum(coefs_bonus_gen) <= 0 and len(coefs_bonus_gen) != 1: log( "comp_etud_moy_gen: invalid or null coefficient (%s) for notes_bonus_gen=%s (etudid=%s, formsemestre_id=%s)" % ( coefs_bonus_gen, notes_bonus_gen, etudid, self.formsemestre_id, ) ) bonus = 0 else: if len(coefs_bonus_gen) == 1: coefs_bonus_gen = [1.0] # irrelevant, may be zero # XXX attention: utilise anciens bonus_sport, évidemment bonus_func = ScoDocSiteConfig.get_bonus_sport_func() if bonus_func: bonus = bonus_func( notes_bonus_gen, coefs_bonus_gen, infos=infos ) else: bonus = 0.0 self.bonus[etudid] = bonus infos["moy"] += bonus infos["moy"] = min(infos["moy"], 20.0) # clip bogus bonus return infos def get_etud_moy_gen(self, etudid): # -> float | str """Moyenne generale de cet etudiant dans ce semestre. Prend en compte les UE capitalisées. Si pas de notes: 'NA' """ return self.moy_gen[etudid] def get_etud_moy_infos(self, etudid): # XXX OBSOLETE """Infos sur moyennes""" return self.etud_moy_infos[etudid] # was etud_has_all_ue_over_threshold: def etud_check_conditions_ues(self, etudid): """Vrai si les conditions sur les UE sont remplies. Ne considère que les UE ayant des notes (moyenne calculée). (les UE sans notes ne sont pas comptées comme sous la barre) Prend en compte les éventuelles UE capitalisées. Pour les parcours habituels, cela revient à vérifier que les moyennes d'UE sont toutes > à leur barre (sauf celles sans notes) Pour les parcours non standards (LP2014), cela peut être plus compliqué. Return: True|False, message explicatif """ ue_status_list = [] for ue in self._ues: ue_status = self.get_etud_ue_status(etudid, ue["ue_id"]) if ue_status: ue_status_list.append(ue_status) return self.parcours.check_barre_ues(ue_status_list) def get_table_moyennes_triees(self): return self.T def get_etud_rang(self, etudid) -> str: return self.etud_moy_gen_ranks.get(etudid, "999") def get_etud_rang_group(self, etudid, group_id): """Returns rank of etud in this group and number of etuds in group. If etud not in group, returns None. """ if not group_id in self.rangs_groupes: # lazy: fill rangs_groupes on demand # { groupe : { etudid : rang } } if not group_id in self.group_etuds: # lazy fill: list of etud in group_id etuds = sco_groups.get_group_members(group_id) self.group_etuds[group_id] = set([x["etudid"] for x in etuds]) # 1- build T restricted to group Tr = [] for t in self.get_table_moyennes_triees(): t_etudid = t[-1] if t_etudid in self.group_etuds[group_id]: Tr.append(t) # self.rangs_groupes[group_id] = comp_ranks(Tr) return ( self.rangs_groupes[group_id].get(etudid, None), len(self.rangs_groupes[group_id]), ) def get_table_moyennes_dict(self): """{ etudid : (liste des moyennes) } comme get_table_moyennes_triees""" D = {} for t in self.T: D[t[-1]] = t return D def get_moduleimpls_attente(self): "Liste des moduleimpls avec des notes en attente" return self._mods_att # Decisions existantes du jury def comp_decisions_jury(self): """Cherche les decisions du jury pour le semestre (pas les UE). Calcule l'attribut: decisions_jury = { etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }} decision_jury_ues={ etudid : { ue_id : { 'code' : Note|ADM|CMP, 'event_date' }}} Si la decision n'a pas été prise, la clé etudid n'est pas présente. Si l'étudiant est défaillant, met un code DEF sur toutes les UE """ cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor.execute( """SELECT etudid, code, assidu, compense_formsemestre_id, event_date FROM scolar_formsemestre_validation WHERE formsemestre_id=%(formsemestre_id)s AND ue_id is NULL; """, {"formsemestre_id": self.formsemestre_id}, ) decisions_jury = {} for ( etudid, code, assidu, compense_formsemestre_id, event_date, ) in cursor.fetchall(): decisions_jury[etudid] = { "code": code, "assidu": assidu, "compense_formsemestre_id": compense_formsemestre_id, "event_date": ndb.DateISOtoDMY(event_date), } self.decisions_jury = decisions_jury # UEs: cursor.execute( "select etudid, ue_id, code, event_date from scolar_formsemestre_validation where formsemestre_id=%(formsemestre_id)s and ue_id is not NULL;", {"formsemestre_id": self.formsemestre_id}, ) decisions_jury_ues = {} for (etudid, ue_id, code, event_date) in cursor.fetchall(): if etudid not in decisions_jury_ues: decisions_jury_ues[etudid] = {} # Calcul des ECTS associes a cette UE: ects = 0.0 if sco_codes_parcours.code_ue_validant(code): ue = self.uedict.get(ue_id, None) if ue is None: # not in list for this sem ??? (probably an error) log( "Warning: %s capitalized an UE %s which is not part of current sem %s" % (etudid, ue_id, self.formsemestre_id) ) ue = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0] self.uedict[ue_id] = ue # record this UE if ue_id not in self._uecoef: cl = formsemestre_uecoef_list( cnx, args={ "formsemestre_id": self.formsemestre_id, "ue_id": ue_id, }, ) if not cl: # cas anormal: UE capitalisee, pas dans ce semestre, et sans coef log("Warning: setting UE coef to zero") formsemestre_uecoef_create( cnx, args={ "formsemestre_id": self.formsemestre_id, "ue_id": ue_id, "coefficient": 0, }, ) ects = ue["ects"] or 0.0 # 0 if None decisions_jury_ues[etudid][ue_id] = { "code": code, "ects": ects, # 0. si non UE validée ou si mode de calcul different (?) "event_date": ndb.DateISOtoDMY(event_date), } self.decisions_jury_ues = decisions_jury_ues def get_etud_decision_sem(self, etudid): """Decision du jury prise pour cet etudiant, ou None s'il n'y en pas eu. { 'code' : None|ATT|..., 'assidu' : 0|1, 'event_date' : , compense_formsemestre_id } Si état défaillant, force le code a DEF """ if self.get_etud_etat(etudid) == DEF: return { "code": DEF, "assidu": False, "event_date": "", "compense_formsemestre_id": None, } else: return self.decisions_jury.get(etudid, None) def get_etud_decision_ues(self, etudid): """Decisions du jury pour les UE de cet etudiant, ou None s'il n'y en pas eu. Ne tient pas compte des UE capitalisées. { ue_id : { 'code' : ADM|CMP|AJ, 'event_date' : } Ne renvoie aucune decision d'UE pour les défaillants """ if self.get_etud_etat(etudid) == DEF: return {} else: return self.decisions_jury_ues.get(etudid, None) def sem_has_decisions(self): """True si au moins une decision de jury dans ce semestre""" if [x for x in self.decisions_jury_ues.values() if x]: return True return len([x for x in self.decisions_jury_ues.values() if x]) > 0 def etud_has_decision(self, etudid): """True s'il y a une décision de jury pour cet étudiant""" return self.get_etud_decision_ues(etudid) or self.get_etud_decision_sem(etudid) def all_etuds_have_sem_decisions(self): """True si tous les étudiants du semestre ont une décision de jury. ne regarde pas les décisions d'UE (todo: à voir ?) """ for etudid in self.get_etudids(): if self.inscrdict[etudid]["etat"] == "D": continue # skip demissionnaires if self.get_etud_decision_sem(etudid) is None: return False return True # Capitalisation des UEs def comp_ue_capitalisees(self): """Cherche pour chaque etudiant ses UE capitalisées dans ce semestre. Calcule l'attribut: ue_capitalisees = { etudid : [{ 'moy':, 'event_date' : ,'formsemestre_id' : }, ...] } """ self.ue_capitalisees = scu.DictDefault(defaultvalue=[]) cnx = None semestre_id = self.sem["semestre_id"] for etudid in self.get_etudids(): capital = formsemestre_get_etud_capitalisation( self.formation["id"], semestre_id, ndb.DateDMYtoISO(self.sem["date_debut"]), etudid, ) for ue_cap in capital: # Si la moyenne d'UE n'avait pas été stockée (anciennes versions de ScoDoc) # il faut la calculer ici et l'enregistrer if ue_cap["moy_ue"] is None: log( "comp_ue_capitalisees: recomputing UE moy (etudid=%s, ue_id=%s formsemestre_id=%s)" % (etudid, ue_cap["ue_id"], ue_cap["formsemestre_id"]) ) nt_cap = sco_cache.NotesTableCache.get( ue_cap["formsemestre_id"] ) # > UE capitalisees par un etud ue_cap_status = nt_cap.get_etud_ue_status(etudid, ue_cap["ue_id"]) if ue_cap_status: moy_ue_cap = ue_cap_status["moy"] else: moy_ue_cap = "" ue_cap["moy_ue"] = moy_ue_cap if ( isinstance(moy_ue_cap, float) and moy_ue_cap >= self.parcours.NOTES_BARRE_VALID_UE ): if not cnx: cnx = ndb.GetDBConnexion() sco_parcours_dut.do_formsemestre_validate_ue( cnx, nt_cap, ue_cap["formsemestre_id"], etudid, ue_cap["ue_id"], ue_cap["code"], ) else: log( "*** valid inconsistency: moy_ue_cap=%s (etudid=%s, ue_id=%s formsemestre_id=%s)" % ( moy_ue_cap, etudid, ue_cap["ue_id"], ue_cap["formsemestre_id"], ) ) ue_cap["moy"] = ue_cap["moy_ue"] # backward compat (needs refactoring) self.ue_capitalisees[etudid].append(ue_cap) if cnx: cnx.commit() # log('comp_ue_capitalisees=\n%s' % pprint.pformat(self.ue_capitalisees) ) # def comp_etud_sum_coef_modules_ue( etudid, ue_id): # """Somme des coefficients des modules de l'UE dans lesquels cet étudiant est inscrit # ou None s'il n'y a aucun module # """ # c_list = [ mod['module']['coefficient'] # for mod in self._modimpls # if (( mod['module']['ue_id'] == ue_id) # and self._modmoys[mod['moduleimpl_id']].get(etudid, False) is not False) # ] # if not c_list: # return None # return sum(c_list) def get_etud_ue_cap_coef(self, etudid, ue, ue_cap, cnx=None): """Calcule le coefficient d'une UE capitalisée, pour cet étudiant, injectée dans le semestre courant. ue : ue du semestre courant ue_cap = resultat de formsemestre_get_etud_capitalisation { 'ue_id' (dans le semestre source), 'ue_code', 'moy', 'event_date','formsemestre_id' } """ # log("get_etud_ue_cap_coef\nformsemestre_id='%s'\netudid='%s'\nue=%s\nue_cap=%s\n" % (self.formsemestre_id, etudid, ue, ue_cap)) # 1- Coefficient explicitement déclaré dans le semestre courant pour cette UE ? if ue["ue_id"] not in self._uecoef: self._uecoef[ue["ue_id"]] = formsemestre_uecoef_list( cnx, args={"formsemestre_id": self.formsemestre_id, "ue_id": ue["ue_id"]}, ) if len(self._uecoef[ue["ue_id"]]): # utilisation du coef manuel return self._uecoef[ue["ue_id"]][0]["coefficient"] # 2- Mode automatique: calcul du coefficient # Capitalisation depuis un autre semestre ScoDoc ? coef = None if ue_cap["formsemestre_id"]: # Somme des coefs dans l'UE du semestre d'origine (nouveau: 23/01/2016) coef = comp_etud_sum_coef_modules_ue( ue_cap["formsemestre_id"], etudid, ue_cap["ue_id"] ) if coef != None: return coef else: # Capitalisation UE externe: quel coef appliquer ? # Si l'étudiant est inscrit dans le semestre courant, # somme des coefs des modules de l'UE auxquels il est inscrit c = comp_etud_sum_coef_modules_ue(self.formsemestre_id, etudid, ue["ue_id"]) if c is not None: # inscrit à au moins un module de cette UE return c # arfff: aucun moyen de déterminer le coefficient de façon sûre log( "* oups: calcul coef UE impossible\nformsemestre_id='%s'\netudid='%s'\nue=%s\nue_cap=%s" % (self.formsemestre_id, etudid, ue, ue_cap) ) raise ScoValueError( """<div class="scovalueerror"><p>Coefficient de l'UE capitalisée %s impossible à déterminer pour l'étudiant <a href="%s" class="discretelink">%s</a></p> <p>Il faut <a href="%s">saisir le coefficient de cette UE avant de continuer</a></p> </div> """ % ( ue["acronyme"], url_for( "scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid ), self.get_nom_long(etudid), url_for( "notes.formsemestre_edit_uecoefs", scodoc_dept=g.scodoc_dept, formsemestre_id=self.formsemestre_id, err_ue_id=ue["ue_id"], ), ) ) return 0.0 # ? def get_etud_ue_status(self, etudid, ue_id): "Etat de cette UE (note, coef, capitalisation, ...)" return self._etud_moy_ues[etudid][ue_id] def etud_has_notes_attente(self, etudid): """Vrai si cet etudiant a au moins une note en attente dans ce semestre. (ne compte que les notes en attente dans des évaluation avec coef. non nul). """ cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor.execute( """SELECT n.* FROM notes_notes n, notes_evaluation e, notes_moduleimpl m, notes_moduleimpl_inscription i WHERE n.etudid = %(etudid)s and n.value = %(code_attente)s and n.evaluation_id = e.id and e.moduleimpl_id = m.id and m.formsemestre_id = %(formsemestre_id)s and e.coefficient != 0 and m.id = i.moduleimpl_id and i.etudid=%(etudid)s """, { "formsemestre_id": self.formsemestre_id, "etudid": etudid, "code_attente": scu.NOTES_ATTENTE, }, ) return len(cursor.fetchall()) > 0 def get_evaluations_etats(self): # evaluation_list_in_sem """[ {...evaluation et son etat...} ]""" if self._evaluations_etats is None: self._evaluations_etats = sco_evaluations.do_evaluation_list_in_sem( self.formsemestre_id ) return self._evaluations_etats def get_mod_evaluation_etat_list(self, moduleimpl_id) -> list[dict]: """Liste des évaluations de ce module""" return [ e for e in self.get_evaluations_etats() if e["moduleimpl_id"] == moduleimpl_id ] def apc_recompute_moyennes(self): """recalcule les moyennes en APC (BUT) et modifie en place le tableau T. XXX Raccord provisoire avant refonte de cette classe. """ assert self.parcours.APC_SAE formsemestre = FormSemestre.query.get(self.formsemestre_id) results = bulletin_but.ResultatsSemestreBUT(formsemestre) # Rappel des épisodes précédents: T est une liste de liste # Colonnes: 0 moy_gen, moy_ue1, ..., moy_ue_n, moy_mod1, ..., moy_mod_n, etudid ues = self.get_ues_stat_dict() # incluant le(s) UE de sport for t in self.T: etudid = t[-1] if etudid in results.etud_moy_gen: # evite les démissionnaires t[0] = results.etud_moy_gen[etudid] for i, ue in enumerate(ues, start=1): if ue["type"] != UE_SPORT: # temporaire pour 9.1.29 ! if ue["id"] in results.etud_moy_ue: t[i] = results.etud_moy_ue[ue["id"]][etudid] else: t[i] = "" # re-trie selon la nouvelle moyenne générale: self.T.sort(key=self._row_key) # Remplace aussi le rang: self.etud_moy_gen_ranks = results.etud_moy_gen_ranks