diff --git a/app/pe/pe_tagtable.py b/app/pe/pe_tagtable.py index e6ddb19c..e14ca6ad 100644 --- a/app/pe/pe_tagtable.py +++ b/app/pe/pe_tagtable.py @@ -40,7 +40,7 @@ Created on Thu Sep 8 09:36:33 2016 import datetime import numpy as np -from app.scodoc import notes_table +from app.scodoc import sco_utils as scu class TableTag(object): @@ -186,7 +186,7 @@ class TableTag(object): if isinstance(col[0], float) else 0, # remplace les None et autres chaines par des zéros ) # triées - self.rangs[tag] = notes_table.comp_ranks(lesMoyennesTriees) # les rangs + self.rangs[tag] = scu.comp_ranks(lesMoyennesTriees) # les rangs # calcul des stats self.comp_stats_d_un_tag(tag) diff --git a/app/scodoc/notes_table.py b/app/scodoc/notes_table.py deleted file mode 100644 index 95369748..00000000 --- a/app/scodoc/notes_table.py +++ /dev/null @@ -1,1356 +0,0 @@ -# -*- mode: python -*- -# -*- coding: utf-8 -*- - -############################################################################## -# -# Gestion scolarite IUT -# -# Copyright (c) 1999 - 2023 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.codes_cursus import ( - DEF, - UE_SPORT, - ue_is_fondamentale, - ue_is_professionnelle, -) -from app.scodoc import sco_cache -from app.scodoc import codes_cursus -from app.scodoc import sco_compute_moy -from app.scodoc.sco_cursus import formsemestre_get_etud_capitalisation -from app.scodoc import sco_cursus_dut -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_etud -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_preferences - - -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 = codes_cursus.get_cursus_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 == scu.INSCRIT: - return "" - elif etat == scu.DEMISSION: - return ' (DEMISSIONNAIRE) ' - elif etat == DEF: - return ' (DEFAILLANT) ' - else: - return ' (%s) ' % 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"] != scu.INSCRIT: - 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"] != scu.INSCRIT: - if self.inscrdict[etudid]["etat"] == scu.DEMISSION: - 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 codes_cursus.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_decisions_ue(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_decisions_ue(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_cursus_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( - """
Coefficient de l'UE capitalisée %s impossible à déterminer - pour l'étudiant %s
-Il faut saisir le coefficient de cette UE avant de continuer
-@@ -1741,7 +1740,7 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None): help, ] # - ues, modimpls = notes_table.get_sem_ues_modimpls(formsemestre_id) + ues, modimpls = _get_sem_ues_modimpls(formsemestre_id) for ue in ues: ue["sum_coefs"] = sum( [ @@ -1865,6 +1864,24 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None): """ +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 + + # ----- identification externe des sessions (pour SOJA et autres logiciels) def get_formsemestre_session_id(sem, code_specialite, parcours): """Identifiant de session pour ce semestre diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 133a5024..f1077b4f 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -577,8 +577,9 @@ def fill_formsemestre(sem): }">{eyeicon}""" else: sem["eyelink"] = "" - F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0] - sem["formation"] = F + sem["formation"] = Formation.query.get_or_404(sem["formation_id"]).to_dict( + with_departement=False + ) parcours = codes_cursus.get_cursus_from_code(F["type_parcours"]) if sem["semestre_id"] != -1: sem["num_sem"] = f""", {parcours.SESSION_NAME} {sem["semestre_id"]}""" diff --git a/app/scodoc/sco_inscr_passage.py b/app/scodoc/sco_inscr_passage.py index 1a5953be..1ebf679f 100644 --- a/app/scodoc/sco_inscr_passage.py +++ b/app/scodoc/sco_inscr_passage.py @@ -36,7 +36,7 @@ from flask import url_for, g, request import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu from app import log -from app.models import FormSemestre +from app.models import Formation, FormSemestre from app.scodoc.gen_tables import GenTable from app.scodoc import html_sco_header from app.scodoc import sco_cache @@ -261,8 +261,8 @@ def list_source_sems(sem, delai=None) -> list[dict]: if s["semestre_id"] == codes_cursus.NO_SEMESTRE_ID: continue # - F = sco_formations.formation_list(args={"formation_id": s["formation_id"]})[0] - parcours = codes_cursus.get_cursus_from_code(F["type_parcours"]) + formation: Formation = Formation.query.get_or_404(s["formation_id"]) + parcours = codes_cursus.get_cursus_from_code(formation.type_parcours) if not parcours.ALLOW_SEM_SKIP: if s["semestre_id"] < (sem["semestre_id"] - 1): continue diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py index 3e6b256d..a70cb0f4 100644 --- a/app/scodoc/sco_moduleimpl_status.py +++ b/app/scodoc/sco_moduleimpl_status.py @@ -38,9 +38,7 @@ from app.auth.models import User from app.comp import res_sem from app.comp.res_common import ResultatsSemestre from app.comp.res_compat import NotesTableCompat -from app.models import FormSemestre, ModuleImpl -from app.models.evaluations import Evaluation -from app.models.ues import UniteEns +from app.models import Evaluation, FormSemestre, Module, ModuleImpl, UniteEns import app.scodoc.sco_utils as scu from app.scodoc.codes_cursus import UE_SPORT from app.scodoc.sco_exceptions import ScoInvalidIdType @@ -51,11 +49,8 @@ from app.scodoc import html_sco_header from app.scodoc import htmlutils from app.scodoc import sco_abs from app.scodoc import sco_compute_moy -from app.scodoc import sco_edit_module from app.scodoc import sco_evaluations from app.scodoc import sco_evaluation_db -from app.scodoc import sco_formations -from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_status from app.scodoc import sco_groups from app.scodoc import sco_moduleimpl @@ -80,7 +75,7 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str: else: sup_label = "Supprimer évaluation" - menuEval = [ + menu_eval = [ { "title": "Saisir notes", "endpoint": "notes.saisie_notes", @@ -159,7 +154,7 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str: }, ] - return htmlutils.make_menu("actions", menuEval, alone=True) + return htmlutils.make_menu("actions", menu_eval, alone=True) def _ue_coefs_html(coefs_lst) -> str: @@ -195,14 +190,9 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): if not isinstance(moduleimpl_id, int): raise ScoInvalidIdType("moduleimpl_id must be an integer !") modimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id) - mi_dict = modimpl.to_dict() + module: Module = modimpl.module formsemestre_id = modimpl.formsemestre_id formsemestre: FormSemestre = modimpl.formsemestre - mod_dict = sco_edit_module.module_list(args={"module_id": modimpl.module_id})[0] - sem = sco_formsemestre.get_formsemestre(formsemestre_id) - formation_dict = sco_formations.formation_list( - args={"formation_id": sem["formation_id"]} - )[0] mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list( moduleimpl_id=moduleimpl_id ) @@ -223,10 +213,10 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): or [0] ) # - sem_locked = not sem["etat"] + sem_locked = not formsemestre.etat can_edit_evals = ( sco_permissions_check.can_edit_notes( - current_user, moduleimpl_id, allow_ens=sem["ens_can_edit_eval"] + current_user, moduleimpl_id, allow_ens=formsemestre.ens_can_edit_eval ) and not sem_locked ) @@ -237,22 +227,22 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags() # module_resp = User.query.get(modimpl.responsable_id) - mod_type_name = scu.MODULE_TYPE_NAMES[mod_dict["module_type"]] + mod_type_name = scu.MODULE_TYPE_NAMES[module.module_type] H = [ html_sco_header.sco_header( - page_title=f"{mod_type_name} {mod_dict['code']} {mod_dict['titre']}", + page_title=f"{mod_type_name} {module.code} {module.titre}", javascripts=["js/etud_info.js"], init_qtip=True, ), - f"""
Responsable: | @@ -281,8 +271,8 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): # 2ieme ligne: Semestre, Coef H.append(""" | |||
""") - if sem["semestre_id"] >= 0: - H.append("""Semestre: | %s""" % sem["semestre_id"]) + if formsemestre.semestre_id >= 0: + H.append("""Semestre: | %s""" % formsemestre.semestre_id) else: H.append(""" | """) if sem_locked: @@ -293,34 +283,34 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): else: H.append( f"""Coef. dans le semestre: { - "non défini" if modimpl.module.coefficient is None else modimpl.module.coefficient + "non défini" if modimpl.module.coefficient is None else modimpl.module.coefficient }""" ) H.append(""" | |
Formation: | %(titre)s | |||
Formation: | {formsemestre.formation.titre} | +|||
Inscrits: | %d étudiants""" - % len(mod_inscrits) + f""" | |||
Inscrits: | {len(mod_inscrits)} étudiants""" ) if current_user.has_permission(Permission.ScoEtudInscrit): H.append( - """modifier""" - % mi_dict["moduleimpl_id"] + f"""modifier""" ) H.append(" | |||
Règle de calcul: moyenne={mi_dict["computation_expr"]} + >moyenne={modimpl.computation_expr} """ ) H.append("""inutilisée dans cette version de ScoDoc""") @@ -335,7 +325,8 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): H.append(" | ||||
' # règle de calcul standard' + ' | ||||
'
+ # règle de calcul standard'
)
# if sco_moduleimpl.can_change_ens(moduleimpl_id, raise_exc=False):
# H.append(
@@ -396,7 +387,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
)
#
# Liste les noms de partitions
- partitions = sco_groups.get_partitions_list(sem["formsemestre_id"])
+ partitions = sco_groups.get_partitions_list(formsemestre.id)
H.append(
"""Afficher les groupes
de
+ f"""
-Voir toutes les notes
+Voir toutes les notes
"""
- % mi_dict
)
# -------- Tableau des evaluations
top_table_links = ""
if can_edit_evals:
top_table_links = f"""Créer nouvelle évaluation
"""
if nb_evaluations > 0:
top_table_links += f"""
Trier par date
"""
diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py
index abdff099..6ff484ca 100644
--- a/app/scodoc/sco_utils.py
+++ b/app/scodoc/sco_utils.py
@@ -545,6 +545,11 @@ def strnone(s):
return ""
+def strip_str(s):
+ "if s is a string, strip it, if is None, do nothing"
+ return s.strip() if s else s
+
+
def stripquotes(s):
"strip s from spaces and quotes"
s = s.strip()
@@ -1136,6 +1141,36 @@ def objects_renumber(db, obj_list) -> None:
db.session.commit()
+def comp_ranks(T: list[tuple]) -> dict[int, str]:
+ """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 gen_cell(key: str, row: dict, elt="td", with_col_class=False):
"html table cell"
klass = row.get(f"_{key}_class", "")
diff --git a/app/views/notes.py b/app/views/notes.py
index 429adf38..545ba9f5 100644
--- a/app/views/notes.py
+++ b/app/views/notes.py
@@ -3383,22 +3383,3 @@ def check_formsemestre_integrity(formsemestre_id):
return (
html_sco_header.sco_header() + " ".join(diag) + html_sco_header.sco_footer() ) - - -@bp.route("/check_integrity_all") -@scodoc -@permission_required(Permission.ScoView) -@scodoc7func -def check_integrity_all(): - "debug: verifie tous les semestres et tt les formations" - # formations - for F in sco_formations.formation_list(): - check_form_integrity(F["formation_id"]) - # semestres - for sem in sco_formsemestre.do_formsemestre_list(): - check_formsemestre_integrity(sem["formsemestre_id"]) - return ( - html_sco_header.sco_header() - + " empty page: see logs and mails " - + html_sco_header.sco_footer() - ) diff --git a/tests/unit/sco_fake_gen.py b/tests/unit/sco_fake_gen.py index c3ec2572..770b6c2a 100644 --- a/tests/unit/sco_fake_gen.py +++ b/tests/unit/sco_fake_gen.py @@ -14,20 +14,16 @@ import sys import string import typing - -from config import Config +from app import db, log from app.auth.models import User -from app.models import FormationModalite, Matiere +from app.models import Formation, FormationModalite, Matiere from app.scodoc import notesdb as ndb from app.scodoc import codes_cursus -from app.scodoc import sco_edit_formation 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_etud -from app.scodoc import sco_evaluations from app.scodoc import sco_evaluation_db -from app.scodoc import sco_formations from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_validation @@ -35,8 +31,8 @@ from app.scodoc import sco_moduleimpl from app.scodoc import sco_saisie_notes from app.scodoc import sco_synchro_etuds from app.scodoc import sco_utils as scu -from app import log from app.scodoc.sco_exceptions import ScoValueError +from config import Config from tests.unit.setup import NOTES_T @@ -161,11 +157,17 @@ class ScoFake(object): """Crée une formation""" if not acronyme: acronyme = "TEST" + str(random.randint(100000, 999999)) - oid = sco_edit_formation.do_formation_create(locals()) - oids = sco_formations.formation_list(formation_id=oid) - if not oids: - raise ScoValueError("formation not created !") - return oid + formation = Formation( + acronyme=scu.strip_str(acronyme), + titre=scu.strip_str(titre), + titre_officiel=scu.strip_str(titre_officiel), + type_parcours=scu.strip_str(type_parcours), + formation_code=scu.strip_str(formation_code), + code_specialite=scu.strip_str(code_specialite), + ) + db.session.add(formation) + db.session.commit() + return formation.id @logging_meth def create_ue( diff --git a/tests/unit/test_formations.py b/tests/unit/test_formations.py index b3aaba0b..b974d99e 100644 --- a/tests/unit/test_formations.py +++ b/tests/unit/test_formations.py @@ -29,7 +29,6 @@ # - create_module # - create_formsemestre # - create_moduleimpl -# - formation_list # - formation_export # - formsemestre_list # - moduleimpl_list @@ -73,7 +72,7 @@ def test_formations(test_client): formation_id = G.create_formation( acronyme="F1", titre="Formation 1", titre_officiel="Titre officiel 1" ) - f = sco_formations.formation_list(formation_id)[0] + f = Formation.query.get(formation_id).to_dict() ue_id = G.create_ue(formation_id=formation_id, acronyme="TST1", titre="ue test") matiere_id = G.create_matiere(ue_id=ue_id, titre="matière test") module_id = G.create_module( @@ -102,7 +101,7 @@ def test_formations(test_client): ) formation_id2 = G.create_formation(acronyme="", titre="Formation test") - formation2 = sco_formations.formation_list(formation_id2)[0] + assert Formation.query.get(formation_id2) ue3 = G.create_ue(formation_id=formation_id2, acronyme="TST3", titre="ue test3") matiere_id4 = G.create_matiere(ue_id=ue3, titre="matière test3") module_id3 = G.create_module( |