diff --git a/app/pe/pe_tagtable.py b/app/pe/pe_tagtable.py index e6ddb19c4b..e14ca6ade4 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 953697484b..0000000000 --- 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

-
- """ - % ( - 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 diff --git a/app/scodoc/sco_compute_moy.py b/app/scodoc/sco_compute_moy.py index fb162170cf..e2642f5e99 100644 --- a/app/scodoc/sco_compute_moy.py +++ b/app/scodoc/sco_compute_moy.py @@ -25,43 +25,22 @@ # ############################################################################## -"""Calcul des moyennes de module +"""Calcul des moyennes de module (restes de fonctions ScoDoc 7) """ -import pprint -import traceback - -from flask import url_for, g -import app.scodoc.sco_utils as scu +from app.models import ModuleImpl import app.scodoc.notesdb as ndb -from app.scodoc.sco_utils import ( - ModuleType, - NOTES_ATTENTE, - NOTES_NEUTRALISE, - EVALUATION_NORMALE, - EVALUATION_RATTRAPAGE, - EVALUATION_SESSION2, -) -from app.scodoc.sco_exceptions import ScoValueError -from app import log -from app.scodoc import sco_abs -from app.scodoc import sco_edit_module -from app.scodoc import sco_evaluation_db -from app.scodoc import sco_formsemestre -from app.scodoc import sco_formsemestre_inscriptions -from app.scodoc import sco_formulas -from app.scodoc import sco_moduleimpl -from app.scodoc import sco_etud -def moduleimpl_has_expression(mod): - "True if we should use a user-defined expression" - expr = mod["computation_expr"] - if not expr: - return False - expr = expr.strip() - if not expr or expr[0] == "#": - return False - return True +def moduleimpl_has_expression(modimpl: ModuleImpl): + """True if we should use a user-defined expression + En ScoDoc 9, utilisé pour afficher un avertissement, l'expression elle même + n'est plus supportée. + """ + return ( + modimpl.computation_expr + and modimpl.computation_expr.strip() + and modimpl.computation_expr.strip()[0] != "#" + ) def formsemestre_expressions_use_abscounts(formsemestre_id): @@ -81,9 +60,10 @@ def formsemestre_expressions_use_abscounts(formsemestre_id): if expr and expr[0] != "#" and ab in expr: return True # 2- moyennes de modules - for mod in sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id): - if moduleimpl_has_expression(mod) and ab in mod["computation_expr"]: - return True + # #sco9 il n'y a plus d'expressions + # for mod in sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id): + # if moduleimpl_has_expression(mod) and ab in mod["computation_expr"]: + # return True return False @@ -120,296 +100,3 @@ def get_ue_expression(formsemestre_id, ue_id, html_quote=False): return expr else: return None - - -def compute_user_formula( - sem, - etudid, - moy, - moy_valid, - notes, - coefs, - coefs_mask, - formula, - diag_info=None, # infos supplementaires a placer ds messages d'erreur - use_abs=True, -): - """Calcul moyenne a partir des notes et coefs, en utilisant la formule utilisateur (une chaine). - Retourne moy, et en cas d'erreur met à jour diag_info (msg) - """ - if use_abs: - nbabs, nbabs_just = sco_abs.get_abs_count(etudid, sem) - else: - nbabs, nbabs_just = 0, 0 - try: - moy_val = float(moy) - except ValueError: - moy_val = 0.0 # 0. when no valid value - variables = { - "cmask": coefs_mask, # NoteVector(v=coefs_mask), - "notes": notes, # NoteVector(v=notes), - "coefs": coefs, # NoteVector(v=coefs), - "moy": moy, - "moy_valid": moy_valid, # deprecated, use moy_is_valid - "moy_is_valid": moy_valid, # True si moyenne numerique - "moy_val": moy_val, - "nb_abs": float(nbabs), - "nb_abs_just": float(nbabs_just), - "nb_abs_nojust": float(nbabs - nbabs_just), - } - try: - formula = formula.replace("\n", "").replace("\r", "") - # log('expression : %s\nvariables=%s\n' % (formula, variables)) # debug - user_moy = sco_formulas.eval_user_expression(formula, variables) - # log('user_moy=%s' % user_moy) - if user_moy != "NA": - user_moy = float(user_moy) - if (user_moy > 20) or (user_moy < 0): - etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] - - raise ScoValueError( - f""" - Valeur moyenne {user_moy} hors limite pour - {etud["nomprenom"]}""" - ) - except: - log( - "invalid expression : %s\nvariables=%s\n" - % (formula, pprint.pformat(variables)) - ) - tb = traceback.format_exc() - log("Exception during evaluation:\n%s\n" % tb) - diag_info.update({"msg": tb.splitlines()[-1]}) - user_moy = "ERR" - - # log('formula=%s\nvariables=%s\nmoy=%s\nuser_moy=%s' % (formula, variables, moy, user_moy)) - - return user_moy - - -# XXX OBSOLETE -def compute_moduleimpl_moyennes(nt, modimpl): - """Retourne dict { etudid : note_moyenne } pour tous les etuds inscrits - au moduleimpl mod, la liste des evaluations "valides" (toutes notes entrées - ou en attente), et att (vrai s'il y a des notes en attente dans ce module). - La moyenne est calculée en utilisant les coefs des évaluations. - Les notes NEUTRES (abs. excuses) ne sont pas prises en compte. - Les notes ABS sont remplacées par des zéros. - S'il manque des notes et que le coef n'est pas nul, - la moyenne n'est pas calculée: NA - Ne prend en compte que les evaluations où toutes les notes sont entrées. - Le résultat note_moyenne est une note sur 20. - """ - diag_info = {} # message d'erreur formule - moduleimpl_id = modimpl["moduleimpl_id"] - is_malus = modimpl["module"]["module_type"] == ModuleType.MALUS - sem = sco_formsemestre.get_formsemestre(modimpl["formsemestre_id"]) - etudids = sco_moduleimpl.moduleimpl_listeetuds( - moduleimpl_id - ) # tous, y compris demissions - # Inscrits au semestre (pour traiter les demissions): - inssem_set = set( - [ - x["etudid"] - for x in sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits( - modimpl["formsemestre_id"] - ) - ] - ) - insmod_set = inssem_set.intersection(etudids) # inscrits au semestre et au module - - evals = nt.get_mod_evaluation_etat_list(moduleimpl_id) - evals.sort( - key=lambda x: (x["numero"], x["jour"], x["heure_debut"]) - ) # la plus ancienne en tête - - user_expr = moduleimpl_has_expression(modimpl) - attente = False - # récupere les notes de toutes les evaluations - eval_rattr = None - for e in evals: - e["nb_inscrits"] = e["etat"]["nb_inscrits"] - # XXX OBSOLETE - notes_db = sco_evaluation_db.do_evaluation_get_all_notes( - e["evaluation_id"] - ) # toutes, y compris demissions - # restreint aux étudiants encore inscrits à ce module - notes = [ - notes_db[etudid]["value"] for etudid in notes_db if (etudid in insmod_set) - ] - e["nb_notes"] = len(notes) - e["nb_abs"] = len([x for x in notes if x is None]) - e["nb_neutre"] = len([x for x in notes if x == NOTES_NEUTRALISE]) - e["nb_att"] = len([x for x in notes if x == NOTES_ATTENTE]) - e["notes"] = notes_db - - if e["etat"]["evalattente"]: - attente = True - if ( - e["evaluation_type"] == EVALUATION_RATTRAPAGE - or e["evaluation_type"] == EVALUATION_SESSION2 - ): - if eval_rattr: - # !!! plusieurs rattrapages ! - diag_info.update( - { - "msg": "plusieurs évaluations de rattrapage !", - "moduleimpl_id": moduleimpl_id, - } - ) - eval_rattr = e - - # Les modules MALUS ne sont jamais considérés en attente - if is_malus: - attente = False - - # filtre les evals valides (toutes les notes entrées) - valid_evals = [ - e - for e in evals - if ( - (e["etat"]["evalcomplete"] or e["etat"]["evalattente"]) - and (e["note_max"] > 0) - ) - ] - # - R = {} - formula = scu.unescape_html(modimpl["computation_expr"]) - formula_use_abs = "abs" in formula - - for etudid in insmod_set: # inscrits au semestre et au module - sum_notes = 0.0 - sum_coefs = 0.0 - nb_missing = 0 - for e in valid_evals: - if e["evaluation_type"] != EVALUATION_NORMALE: - continue - if etudid in e["notes"]: - note = e["notes"][etudid]["value"] - if note is None: # ABSENT - note = 0 - if note != NOTES_NEUTRALISE and note != NOTES_ATTENTE: - sum_notes += (note * 20.0 / e["note_max"]) * e["coefficient"] - sum_coefs += e["coefficient"] - else: - # il manque une note ! (si publish_incomplete, cela peut arriver, on ignore) - if e["coefficient"] > 0 and not e["publish_incomplete"]: - nb_missing += 1 - # ne devrait pas arriver ? - log("\nXXX SCM298\n") - if nb_missing == 0 and sum_coefs > 0: - if sum_coefs > 0: - R[etudid] = sum_notes / sum_coefs - moy_valid = True - else: - R[etudid] = "NA" - moy_valid = False - else: - R[etudid] = "NA" - moy_valid = False - - if user_expr: - # recalcule la moyenne en utilisant la formule utilisateur - notes = [] - coefs = [] - coefs_mask = [] # 0/1, 0 si coef a ete annulé - nb_notes = 0 # nombre de notes valides - for e in evals: - if ( - (e["etat"]["evalcomplete"] or e["etat"]["evalattente"]) - and etudid in e["notes"] - ) and (e["note_max"] > 0): - note = e["notes"][etudid]["value"] - if note is None: - note = 0 - if note != NOTES_NEUTRALISE and note != NOTES_ATTENTE: - notes.append(note * 20.0 / e["note_max"]) - coefs.append(e["coefficient"]) - coefs_mask.append(1) - nb_notes += 1 - else: - notes.append(0.0) - coefs.append(0.0) - coefs_mask.append(0) - else: - notes.append(0.0) - coefs.append(0.0) - coefs_mask.append(0) - if nb_notes > 0 or formula_use_abs: - user_moy = compute_user_formula( - sem, - etudid, - R[etudid], - moy_valid, - notes, - coefs, - coefs_mask, - formula, - diag_info=diag_info, - use_abs=formula_use_abs, - ) - if diag_info: - diag_info["moduleimpl_id"] = moduleimpl_id - R[etudid] = user_moy - # Note de rattrapage ou deuxième session ? - if eval_rattr: - if etudid in eval_rattr["notes"]: - note = eval_rattr["notes"][etudid]["value"] - if note != None and note != NOTES_NEUTRALISE and note != NOTES_ATTENTE: - if not isinstance(R[etudid], float): - R[etudid] = note - else: - note_sur_20 = note * 20.0 / eval_rattr["note_max"] - if eval_rattr["evaluation_type"] == EVALUATION_RATTRAPAGE: - # rattrapage classique: prend la meilleure note entre moyenne - # module et note eval rattrapage - if (R[etudid] == "NA") or (note_sur_20 > R[etudid]): - # log('note_sur_20=%s' % note_sur_20) - R[etudid] = note_sur_20 - elif eval_rattr["evaluation_type"] == EVALUATION_SESSION2: - # rattrapage type "deuxième session": remplace la note moyenne - R[etudid] = note_sur_20 - - return R, valid_evals, attente, diag_info - - -def formsemestre_compute_modimpls_moyennes(nt, formsemestre_id): - """retourne dict { moduleimpl_id : { etudid, note_moyenne_dans_ce_module } }, - la liste des moduleimpls, la liste des evaluations valides, - liste des moduleimpls avec notes en attente. - """ - # sem = sco_formsemestre.get_formsemestre( formsemestre_id) - # inscr = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( - # args={"formsemestre_id": formsemestre_id} - # ) - # etudids = [x["etudid"] for x in inscr] - modimpls = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id) - # recupere les moyennes des etudiants de tous les modules - D = {} - valid_evals = [] - valid_evals_per_mod = {} # { moduleimpl_id : eval } - mods_att = [] - expr_diags = [] - for modimpl in modimpls: - mod = sco_edit_module.module_list(args={"module_id": modimpl["module_id"]})[0] - modimpl["module"] = mod # add module dict to moduleimpl (used by nt) - moduleimpl_id = modimpl["moduleimpl_id"] - assert moduleimpl_id not in D - ( - D[moduleimpl_id], - valid_evals_mod, - attente, - expr_diag, - ) = compute_moduleimpl_moyennes(nt, modimpl) - valid_evals_per_mod[moduleimpl_id] = valid_evals_mod - valid_evals += valid_evals_mod - if attente: - mods_att.append(modimpl) - if expr_diag: - expr_diags.append(expr_diag) - # - return D, modimpls, valid_evals_per_mod, valid_evals, mods_att, expr_diags diff --git a/app/scodoc/sco_edit_formation.py b/app/scodoc/sco_edit_formation.py index 11c31436c0..e712a8bc10 100644 --- a/app/scodoc/sco_edit_formation.py +++ b/app/scodoc/sco_edit_formation.py @@ -29,18 +29,16 @@ (portage from DTML) """ import flask -from flask import g, url_for, request +from flask import flash, g, url_for, request import sqlalchemy from app import db -from app import log from app.models import SHORT_STR_LEN from app.models.formations import Formation from app.models.modules import Module from app.models.ues import UniteEns from app.models import ScolarNews -import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message from app.scodoc.sco_exceptions import ScoValueError, ScoNonEmptyFormationObject @@ -49,7 +47,6 @@ from app.scodoc import html_sco_header from app.scodoc import sco_cache from app.scodoc import codes_cursus from app.scodoc import sco_edit_ue -from app.scodoc import sco_formations from app.scodoc import sco_formsemestre @@ -283,38 +280,55 @@ def formation_edit(formation_id=None, create=False): ) # if create: - formation_id = do_formation_create(tf[2]) + formation = do_formation_create(tf[2]) else: do_formation_edit(tf[2]) + flash( + f"""Création de la formation { + formation.titre} ({formation.acronyme}) version {formation.version}""" + ) return flask.redirect( url_for( - "notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation_id + "notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation.id ) ) -def do_formation_create(args): +def do_formation_create(args: dict) -> Formation: "create a formation" - cnx = ndb.GetDBConnexion() - # check unique acronyme/titre/version - a = args.copy() - if "formation_id" in a: - del a["formation_id"] - f_dicts = sco_formations.formation_list(args=a) - if len(f_dicts) > 0: - log(f"do_formation_create: error: {len(f_dicts)} formations matching args={a}") - raise ScoValueError(f"Formation non unique ({a}) !") - # Si pas de formation_code, l'enleve (default SQL) - if "formation_code" in args and not args["formation_code"]: - del args["formation_code"] - # - r = sco_formations._formationEditor.create(cnx, args) + formation = Formation( + dept_id=g.scodoc_dept_id, + acronyme=args["acronyme"].strip(), + titre=args["titre"].strip(), + titre_officiel=args["titre_officiel"].strip(), + version=args.get("version"), + commentaire=scu.strip_str(args["commentaire"]), + formation_code=args.get("formation_code", "").strip() or None, + type_parcours=args.get("type_parcours"), + code_specialite=args.get("code_specialite").strip() or None, + referentiel_competence_id=args.get("referentiel_competence_id"), + ) + db.session.add(formation) + + try: + db.session.commit() + except sqlalchemy.exc.IntegrityError as exc: + db.session.rollback() + raise ScoValueError( + "On ne peut pas créer deux formations avec mêmes acronymes, titres et versions !", + dest_url=url_for( + "notes.formation_edit", + scodoc_dept=g.scodoc_dept, + formation_id=formation.id, + ), + ) from exc ScolarNews.add( typ=ScolarNews.NEWS_FORM, - text="Création de la formation %(titre)s (%(acronyme)s)" % args, + text=f"""Création de la formation { + formation.titre} ({formation.acronyme}) version {formation.version}""", ) - return r + return formation def do_formation_edit(args): diff --git a/app/scodoc/sco_formations.py b/app/scodoc/sco_formations.py index b9a44d3a36..9022169def 100644 --- a/app/scodoc/sco_formations.py +++ b/app/scodoc/sco_formations.py @@ -303,8 +303,8 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False): f_dict["version"] = version + 1 # create formation - formation_id = sco_edit_formation.do_formation_create(f_dict) - log(f"formation {formation_id} created") + formation = sco_edit_formation.do_formation_create(f_dict) + log(f"formation {formation.id} created") ues_old2new = {} # xml ue_id : new ue_id modules_old2new = {} # xml module_id : new module_id @@ -316,7 +316,7 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False): # -- create UEs for ue_info in D[2]: assert ue_info[0] == "ue" - ue_info[1]["formation_id"] = formation_id + ue_info[1]["formation_id"] = formation.id if "ue_id" in ue_info[1]: xml_ue_id = int(ue_info[1]["ue_id"]) del ue_info[1]["ue_id"] @@ -365,7 +365,7 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False): del mod_info[1]["module_id"] else: xml_module_id = None - mod_info[1]["formation_id"] = formation_id + mod_info[1]["formation_id"] = formation.id mod_info[1]["matiere_id"] = mat_id mod_info[1]["ue_id"] = ue_id if not "module_type" in mod_info[1]: @@ -428,14 +428,15 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False): } module.set_ue_coef_dict(ue_coef_dict) db.session.commit() - return formation_id, modules_old2new, ues_old2new + return formation.id, modules_old2new, ues_old2new -def formation_list_table(formation_id=None, args={}): +def formation_list_table(formation_id=None, args: dict = None): """List formation, grouped by titre and sorted by versions and listing associated semestres returns a table """ + args = args or {} formations = formation_list(formation_id=formation_id, args=args) title = "Programmes pédagogiques" lockicon = scu.icontag( diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index bc7a26d8d5..e262f84568 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -53,6 +53,7 @@ from app.scodoc import html_sco_header from app.scodoc import codes_cursus from app.scodoc import sco_compute_moy 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_evaluation_db from app.scodoc import sco_formations @@ -1707,12 +1708,10 @@ def formsemestre_change_publication_bul( def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None): """Changement manuel des coefficients des UE capitalisées.""" - from app.scodoc import notes_table ok, err = sco_permissions_check.check_access_diretud(formsemestre_id) if not ok: return err - sem = sco_formsemestre.get_formsemestre(formsemestre_id) footer = html_sco_header.sco_footer() help = """

@@ -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 133a50240a..f1077b4f4e 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 1a5953be29..1ebf679fb1 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 3e6b256d89..a70cb0f49d 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"""

{mod_type_name} - {mod_dict['code']} {mod_dict['titre']} - {"dans l'UE " + modimpl.module.ue.acronyme - if modimpl.module.module_type == scu.ModuleType.MALUS + f"""

{mod_type_name} + {module.code} {module.titre} + {"dans l'UE " + modimpl.module.ue.acronyme + if modimpl.module.module_type == scu.ModuleType.MALUS else "" }

+ scu.ModuleType(module.module_type).name.lower()}"> """) # 3ieme ligne: Formation H.append( - """""" - % formation_dict + f""" + + + """ ) # Ligne: Inscrits H.append( - """") # Ligne: règle de calcul - has_expression = sco_compute_moy.moduleimpl_has_expression(mi_dict) + has_expression = sco_compute_moy.moduleimpl_has_expression(modimpl) if has_expression: H.append( f"""") else: H.append( - '
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 abdff099e6..6ff484ca8f 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 429adf387b..545ba9f5b7 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 c3ec257251..770b6c2a85 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 b3aaba0bcc..b974d99e33 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(