From 4469d61221647021d61bd01804c3a0dab9496f7f Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 2 Feb 2022 21:13:39 +0100 Subject: [PATCH 1/8] =?UTF-8?q?am=C3=A9lioration=20formulaire=20cr=C3=A9at?= =?UTF-8?q?ion=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/bulletin_but.py | 1 - app/scodoc/sco_edit_module.py | 38 ++----------- app/scodoc/sco_edit_ue.py | 78 ++++++++++++++++++-------- app/static/css/scodoc.css | 46 +++++++++------ app/templates/scodoc/help/modules.html | 37 ++++++------ 5 files changed, 109 insertions(+), 91 deletions(-) diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index 9441c0316..81a7e1996 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -227,7 +227,6 @@ class BulletinBUT(ResultatsSemestreBUT): "date_debut": formsemestre.date_debut.isoformat(), "date_fin": formsemestre.date_fin.isoformat(), "annee_universitaire": self.formsemestre.annee_scolaire_str(), - "inscription": "TODO-MM-JJ", # XXX TODO "numero": formsemestre.semestre_id, "groupes": [], # XXX TODO "absences": { diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index ab5292a78..a0c7b81f2 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -123,9 +123,6 @@ def module_create( Sinon, donne le choix de l'UE de rattachement et utilise la première matière de cette UE (si elle n'existe pas, la crée). """ - from app.scodoc import sco_formations - from app.scodoc import sco_edit_ue - if matiere_id: matiere = Matiere.query.get_or_404(matiere_id) ue = matiere.ue @@ -160,7 +157,7 @@ def module_create( else: H += [ f"""

Création {object_name} dans la matière {matiere.titre}, - (UE {ue.acronyme})

+ (UE {ue.acronyme}), semestre {ue.semestre_idx} """ ] @@ -534,19 +531,15 @@ def module_edit(module_id=None): formsemestres=FormSemestre.query.filter( ModuleImpl.formsemestre_id == FormSemestre.id, ModuleImpl.module_id == module_id, - ).all(), + ) + .order_by(FormSemestre.date_debut) + .all(), ), ] if not unlocked: H.append( """
Formation verrouillée, seuls certains éléments peuvent être modifiés
""" ) - if in_use: - H.append( - """
Module déjà utilisé dans des semestres, - soyez prudents ! -
""" - ) if is_apc: module_types = scu.ModuleType # tous les types else: @@ -728,30 +721,9 @@ def module_edit(module_id=None): initvalues=module, submitlabel="Modifier ce module", ) - # Affiche liste des formseemstre utilisant ce module - if in_use: - formsemestre_ids = {modimpl.formsemestre_id for modimpl in a_module.modimpls} - formsemestres = [FormSemestre.query.get(fid) for fid in formsemestre_ids] - formsemestres.sort(key=lambda f: f.date_debut) - items = [ - f"""{f.titre}""" - for f in formsemestres - ] - sem_descr = f""" -
-
Ce module est utilisé dans les formsemestres suivants:
- -
- """ - else: - sem_descr = "" # if tf[0] == 0: - return "\n".join(H) + tf[1] + sem_descr + html_sco_header.sco_footer() + return "\n".join(H) + tf[1] + html_sco_header.sco_footer() elif tf[0] == -1: return flask.redirect( url_for( diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index 0615db1e8..e415d2ed7 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -244,18 +244,19 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No """Formulaire modification ou création d'une UE""" create = int(create) if not create: - U = ue_list(args={"ue_id": ue_id}) - if not U: - raise ScoValueError("UE inexistante !") - U = U[0] - formation_id = U["formation_id"] - title = "Modification de l'UE %(titre)s" % U - initvalues = U + ue: UniteEns = UniteEns.query.get_or_404(ue_id) + ue_dict = ue.to_dict() + formation_id = ue.formation_id + title = f"Modification de l'UE {ue.acronyme} {ue.titre}" + initvalues = ue_dict submitlabel = "Modifier les valeurs" + can_change_semestre_id = ue.modules.count() == 0 else: + ue = None title = "Création d'une UE" initvalues = {"semestre_idx": default_semestre_idx} submitlabel = "Créer cette UE" + can_change_semestre_id = True formation = Formation.query.get(formation_id) if not formation: raise ScoValueError(f"Formation inexistante ! (id={formation_id})") @@ -282,7 +283,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No ue_types_names = [sco_codes_parcours.UE_TYPE_NAME[k] for k in ue_types] ue_types = [str(x) for x in ue_types] - fw = [ + form_descr = [ ("ue_id", {"input_type": "hidden"}), ("create", {"input_type": "hidden", "default": create}), ("formation_id", {"input_type": "hidden", "default": formation_id}), @@ -296,18 +297,28 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No "type": "int", }, ), - ( - "semestre_idx", - { - "input_type": "menu", - "type": "int", - "allow_null": False, - "title": parcours.SESSION_NAME.capitalize(), - "explanation": "%s de l'UE dans la formation" % parcours.SESSION_NAME, - "labels": ["non spécifié"] + [str(x) for x in semestres_indices], - "allowed_values": [""] + semestres_indices, - }, - ), + ] + if can_change_semestre_id: + form_descr += [ + ( + "semestre_idx", + { + "input_type": "menu", + "type": "int", + "allow_null": False, + "title": parcours.SESSION_NAME.capitalize(), + "explanation": "%s de l'UE dans la formation" + % parcours.SESSION_NAME, + "labels": ["non spécifié"] + [str(x) for x in semestres_indices], + "allowed_values": [""] + semestres_indices, + }, + ), + ] + else: + form_descr += [ + ("semestre_idx", {"default": ue.semestre_idx, "input_type": "hidden"}), + ] + form_descr += [ ( "type", { @@ -377,7 +388,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No ), ] if create and not parcours.UE_IS_MODULE and not is_apc: - fw.append( + form_descr.append( ( "create_matiere", { @@ -391,14 +402,33 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No tf = TrivialFormulator( request.base_url, scu.get_request_args(), - fw, + form_descr, initvalues=initvalues, submitlabel=submitlabel, ) if tf[0] == 0: - ue_div = """
""" + if ue and ue.modules.count(): + modules_div = f"""
+
{ue.modules.count()} modules sont rattachés + à cette UE du semestre S{ue.semestre_idx}, + elle ne peut donc pas être changée de semestre.
+
""" + else: + modules_div = "" bonus_div = """
""" - return "\n".join(H) + tf[1] + bonus_div + ue_div + html_sco_header.sco_footer() + ue_div = """
""" + return ( + "\n".join(H) + + tf[1] + + modules_div + + bonus_div + + ue_div + + html_sco_header.sco_footer() + ) else: if create: if not tf[2]["ue_code"]: diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index c5758cb5f..8ca51f7b5 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -1771,40 +1771,54 @@ ul.notes_module_list { } div#ue_list_code { - background-color: rgb(220,220,220); - font-size: small; - padding-left: 4px; - padding-bottom: 1px; + background-color: rgb(155, 218, 155); + padding: 10px; + border: 1px solid blue; + border-radius: 10px; + padding: 10px; margin-top: 10px; - margin: 3ex; + margin-right: 15px; } ul.notes_module_list { list-style-type: none; } +div#ue_list_modules { + background-color: rgb(191, 242, 255); + border: 1px solid blue; + border-radius: 10px; + padding: 10px; + margin-top: 10px; + margin-right: 15px; +} + div#ue_list_etud_validations { - background-color: rgb(220,250,220); - padding-left: 4px; - padding-bottom: 1px; - margin: 3ex; + background-color: rgb(220,250,220); + padding-left: 4px; + padding-bottom: 1px; + margin: 3ex; } div#ue_list_etud_validations span { font-weight: bold; } span.ue_share { - font-weight: bold; + font-weight: bold; } div.ue_warning { - border: 1px solid red; - background-color: rgb(250,220,220); - margin: 3ex; - padding-left: 1ex; - padding-right: 1ex; + border: 1px solid red; + border-radius: 10px; + background-color: rgb(250,220,220); + margin-top: 10px; + margin-right: 15px; + margin-bottom: 10px; + padding: 10px; +} +div.ue_warning:first-child { + font-weight: bold; } - div.ue_warning span:before { content: url(/ScoDoc/static/icons/warning_img.png); vertical-align: -80%; diff --git a/app/templates/scodoc/help/modules.html b/app/templates/scodoc/help/modules.html index cd6e0767b..924e128f1 100644 --- a/app/templates/scodoc/help/modules.html +++ b/app/templates/scodoc/help/modules.html @@ -1,3 +1,4 @@ +{# -*- mode: jinja-html -*- #}

Les modules sont décrits dans le programme pédagogique. Un module est pour ce @@ -24,24 +25,26 @@ la documentation.

{%endif%} +
- {% if formsemestres %} -

- Ce module est utilisé dans des semestres déjà mis en place, il faut prêter attention - aux conséquences des changements effectués ici: par exemple les coefficients vont modifier - les notes moyennes calculées. Les modules déjà utilisés ne peuvent pas être changés de semestre, ni détruits. - Si vous souhaitez faire cela, allez d'abord modifier les semestres concernés pour déselectionner le module. -

-

Semestres utilisant ce module:

- + +{%endif%} - \ No newline at end of file From a780b1e78a86ff17ab1e59f330c55c905c877387 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 2 Feb 2022 22:04:18 +0100 Subject: [PATCH 2/8] =?UTF-8?q?Couleurs=20par=20d=C3=A9faut=20des=20UEs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_edit_ue.py | 56 +++++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index e415d2ed7..4fb2eb58e 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -254,7 +254,10 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No else: ue = None title = "Création d'une UE" - initvalues = {"semestre_idx": default_semestre_idx} + initvalues = { + "semestre_idx": default_semestre_idx, + "color": ue_guess_color_default(formation_id, default_semestre_idx), + } submitlabel = "Créer cette UE" can_change_semestre_id = True formation = Formation.query.get(formation_id) @@ -577,14 +580,15 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list formation_id=formation_id, is_external=True ) if is_apc: - # pour faciliter la transition des anciens programmes non APC + # Pour faciliter la transition des anciens programmes non APC for ue in ues_obj: ue.guess_semestre_idx() - # vérifie qu'on a bien au moins une matière dans chaque UE - for ue in ues_obj: + # vérifie qu'on a bien au moins une matière dans chaque UE if ue.matieres.count() < 1: mat = Matiere(ue_id=ue.id) db.session.add(mat) + # donne des couleurs aux UEs crées avant + colorie_anciennes_ues(ues_obj) db.session.commit() ues = [ue.to_dict() for ue in ues_obj] ues_externes = [ue.to_dict() for ue in ues_externes_obj] @@ -1400,7 +1404,7 @@ def formation_table_recap(formation_id, format="html"): return tab.make_page(format=format) -def ue_list_semestre_ids(ue): +def ue_list_semestre_ids(ue: dict): """Liste triée des numeros de semestres des modules dans cette UE Il est recommandable que tous les modules d'une UE aient le même indice de semestre. Mais cela n'a pas toujours été le cas dans les programmes pédagogiques officiels, @@ -1408,3 +1412,45 @@ def ue_list_semestre_ids(ue): """ modules = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]}) return sorted(list(set([mod["semestre_id"] for mod in modules]))) + + +UE_PALETTE = [ + "#EFA00B", + "#99C24D", + "#EC9192", + "#0075C4", + "#D65108", + "#DEC0F1", + "#B02E0C", + "#151E3F", + "#FB3640", +] + + +def colorie_anciennes_ues(ues: list[UniteEns]) -> None: + """Avant ScoDoc 9.2, les ue n'avaient pas de couleurs + Met des défauts raisonnables + """ + nb_colors = len(UE_PALETTE) + index = 0 + last_sem_idx = 0 + for ue in ues: + if ue.semestre_idx != last_sem_idx: + last_sem_idx = ue.semestre_idx + index = 0 + if ue.color is None: + ue.color = UE_PALETTE[index % nb_colors] + index += 1 + db.session.add(ue) + + +def ue_guess_color_default(formation_id: int, default_semestre_idx: int) -> str: + """Un code couleur pour une nouvelle UE dans ce semestre""" + nb_colors = len(UE_PALETTE) + # UE existantes dans ce semestre: + nb_ues = UniteEns.query.filter( + UniteEns.formation_id == formation_id, + UniteEns.semestre_idx == default_semestre_idx, + ).count() + index = nb_ues + return UE_PALETTE[index % nb_colors] From a8367a84fbefaedd3c19b2caffe52b734f605d2e Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 2 Feb 2022 22:22:56 +0100 Subject: [PATCH 3/8] =?UTF-8?q?Fiche=20=C3=A9tudiant:=20utilise=20res=5Fse?= =?UTF-8?q?m,=20affiche=20moyennes=20BUT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/bulletin_but.py | 1 + app/comp/aux_stats.py | 2 +- app/comp/res_common.py | 5 +++-- app/scodoc/sco_formsemestre_validation.py | 18 ++++++++++++------ 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index 81a7e1996..db05abfd4 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -228,6 +228,7 @@ class BulletinBUT(ResultatsSemestreBUT): "date_fin": formsemestre.date_fin.isoformat(), "annee_universitaire": self.formsemestre.annee_scolaire_str(), "numero": formsemestre.semestre_id, + "inscription": "", # inutilisé mais nécessaire pour le js de Seb. "groupes": [], # XXX TODO "absences": { "injustifie": nbabsjust, diff --git a/app/comp/aux_stats.py b/app/comp/aux_stats.py index 07517f362..3337c2b81 100644 --- a/app/comp/aux_stats.py +++ b/app/comp/aux_stats.py @@ -21,7 +21,7 @@ class StatsMoyenne: Les valeurs NAN ou non numériques sont toujours enlevées. Si vals is None, renvoie des zéros (utilisé pour UE bonus) """ - if vals is None: + if vals is None or len(vals) == 0: self.moy = self.min = self.max = self.size = self.nb_vals = 0 else: self.moy = np.nanmean(vals) diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 47caaa03a..601f76a99 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -293,10 +293,11 @@ class NotesTableCompat(ResultatsSemestre): def get_etud_ue_status(self, etudid: int, ue_id: int): coef_ue = self.etud_coef_ue_df[ue_id][etudid] return { + "is_capitalized": False, # XXX TODO + "is_external": False, # XXX TODO + "coef_ue": coef_ue, # XXX TODO "cur_moy_ue": self.etud_moy_ue[ue_id][etudid], "moy": self.etud_moy_ue[ue_id][etudid], - "is_capitalized": False, # XXX TODO - "coef_ue": coef_ue, # XXX TODO } def get_etud_rang(self, etudid: int): diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py index baee42f3c..ed5325ce8 100644 --- a/app/scodoc/sco_formsemestre_validation.py +++ b/app/scodoc/sco_formsemestre_validation.py @@ -31,15 +31,21 @@ import time import flask from flask import url_for, g, request +from app.api.sco_api import formsemestre import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu from app import log -from app.scodoc.scolog import logdb -from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message -from app.scodoc.sco_exceptions import ScoValueError +from app.comp import res_sem +from app.comp.res_common import NotesTableCompat +from app.models import FormSemestre + +from app.scodoc.sco_exceptions import ScoValueError +from app.scodoc.scolog import logdb from app.scodoc.sco_codes_parcours import * +from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message + from app.scodoc import html_sco_header from app.scodoc import sco_abs from app.scodoc import sco_codes_parcours @@ -542,9 +548,9 @@ def formsemestre_recap_parcours_table( else: ass = "" - nt = sco_cache.NotesTableCache.get( - sem["formsemestre_id"] - ) # > get_ues_stat_dict, get_etud_moy_gen, get_etud_ue_status + formsemestre = FormSemestre.query.get(sem["formsemestre_id"]) + nt: NotesTableCompat = res_sem.load_formsemestre_result(formsemestre) + # nt = sco_cache.NotesTableCache.get(sem["formsemestre_id"]) if is_cur: type_sem = "*" # now unused class_sem = "sem_courant" From 5467ad2437921adc26d1c51c9c1b709de40915d9 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 4 Feb 2022 11:19:01 +0100 Subject: [PATCH 4/8] Fix: saisie si aucun etudiant --- app/views/absences.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/views/absences.py b/app/views/absences.py index a9ec8570b..07f4bfdba 100644 --- a/app/views/absences.py +++ b/app/views/absences.py @@ -496,6 +496,7 @@ def SignaleAbsenceGrSemestre( require_module = sco_preferences.get_preference( "abs_require_module", formsemestre_id ) + sem = sco_formsemestre.do_formsemestre_list({"formsemestre_id": formsemestre_id})[0] etuds = [ sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0] for m in groups_infos.members @@ -526,9 +527,7 @@ def SignaleAbsenceGrSemestre( if etuds: nt = sco_cache.NotesTableCache.get(formsemestre_id) - sem = sco_formsemestre.do_formsemestre_list( - {"formsemestre_id": formsemestre_id} - )[0] + work_saturday = sco_abs.is_work_saturday() jourdebut = sco_abs.ddmmyyyy(datedebut, work_saturday=work_saturday) jourfin = sco_abs.ddmmyyyy(datefin, work_saturday=work_saturday) From e6bd6cf28abecb98b31164176f147eb7d144d237 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 6 Feb 2022 16:09:17 +0100 Subject: [PATCH 5/8] WIP: validations d'UE et de semestres --- app/comp/jury.py | 147 ++++++++++++++++++++++ app/comp/moy_mod.py | 13 +- app/comp/res_cache.py | 34 +++++ app/comp/res_common.py | 144 +++++++++++++++------ app/comp/res_sem.py | 32 +++-- app/models/__init__.py | 8 +- app/models/formsemestre.py | 2 +- app/models/notes.py | 94 -------------- app/models/validations.py | 109 ++++++++++++++++ app/scodoc/notes_table.py | 17 ++- app/scodoc/sco_bulletins.py | 2 +- app/scodoc/sco_cache.py | 15 ++- app/scodoc/sco_codes_parcours.py | 9 +- app/scodoc/sco_edit_apc.py | 2 +- app/scodoc/sco_evaluations.py | 5 +- app/scodoc/sco_formsemestre_status.py | 6 +- app/scodoc/sco_formsemestre_validation.py | 8 +- app/scodoc/sco_liste_notes.py | 2 +- app/scodoc/sco_parcours_dut.py | 47 ++++--- app/scodoc/sco_pvjury.py | 15 +-- app/scodoc/sco_recapcomplet.py | 105 +++++++--------- app/views/notes.py | 2 +- 22 files changed, 562 insertions(+), 256 deletions(-) create mode 100644 app/comp/jury.py create mode 100644 app/comp/res_cache.py create mode 100644 app/models/validations.py diff --git a/app/comp/jury.py b/app/comp/jury.py new file mode 100644 index 000000000..bb7cd0d4e --- /dev/null +++ b/app/comp/jury.py @@ -0,0 +1,147 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +"""Stockage des décisions de jury +""" +import pandas as pd + +from app import db +from app.models import FormSemestre, ScolarFormSemestreValidation +from app.comp.res_cache import ResultatsCache +from app.scodoc import sco_cache +from app.scodoc import sco_codes_parcours + + +class ValidationsSemestre(ResultatsCache): + """ """ + + _cached_attrs = ( + "decisions_jury", + "decisions_jury_ues", + "ue_capitalisees", + ) + + def __init__(self, formsemestre: FormSemestre): + super().__init__(formsemestre, sco_cache.ValidationsSemestreCache) + + self.decisions_jury = {} + """Décisions prises dans ce semestre: + { etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }}""" + self.decisions_jury_ues = {} + """Décisions sur des UEs dans ce semestre: + { etudid : { ue_id : { 'code' : Note|ADM|CMP, 'event_date' }}} + """ + + if not self.load_cached(): + self.compute() + self.store() + + def compute(self): + "Charge les résultats de jury et UEs capitalisées" + self.ue_capitalisees = formsemestre_get_ue_capitalisees(self.formsemestre) + self.comp_decisions_jury() + + def comp_decisions_jury(self): + """Cherche les decisions du jury pour le semestre (pas les UE). + Calcule les attributs: + decisions_jury = { etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }} + decision_jury_ues={ etudid : { ue_id : { 'code' : Note|ADM|CMP, 'event_date' }}} + Si la décision n'a pas été prise, la clé etudid n'est pas présente. + Si l'étudiant est défaillant, pas de décisions d'UE. + """ + # repris de NotesTable.comp_decisions_jury pour la compatibilité + decisions_jury_q = ScolarFormSemestreValidation.query.filter_by( + formsemestre_id=self.formsemestre.id + ) + decisions_jury = {} + for decision in decisions_jury_q.filter( + ScolarFormSemestreValidation.ue_id == None # slt dec. sem. + ): + decisions_jury[decision.etudid] = { + "code": decision.code, + "assidu": decision.assidu, + "compense_formsemestre_id": decision.compense_formsemestre_id, + "event_date": decision.event_date.strftime("%d/%m/%Y"), + } + self.decisions_jury = decisions_jury + + # UEs: + decisions_jury_ues = {} + for decision in decisions_jury_q.filter( + ScolarFormSemestreValidation.ue_id != None # slt dec. sem. + ): + if decision.etudid not in decisions_jury_ues: + decisions_jury_ues[decision.etudid] = {} + # Calcul des ECTS associés à cette UE: + if sco_codes_parcours.code_ue_validant(decision.code): + ects = decision.ue.ects or 0.0 # 0 if None + else: + ects = 0.0 + + decisions_jury_ues[decision.etudid][decision.ue.id] = { + "code": decision.code, + "ects": ects, # 0. si UE non validée + "event_date": decision.event_date.strftime("%d/%m/%Y"), + } + + self.decisions_jury_ues = decisions_jury_ues + + +def formsemestre_get_ue_capitalisees(formsemestre: FormSemestre) -> pd.DataFrame: + """Liste des UE capitalisées (ADM) utilisables dans ce formsemestre + + Recherche dans les semestres des formations de même code, avec le même semestre_id + et une date de début antérieure que celle du formsemestre. + Prend aussi les UE externes validées. + + Attention: fonction très coûteuse, cacher le résultat. + + Résultat: DataFrame avec + etudid (index) + formsemestre_id : origine de l'UE capitalisée + is_external : vrai si validation effectuée dans un semestre extérieur + ue_id : dans le semestre origine (pas toujours de la même formation) + ue_code : code de l'UE + moy_ue : + event_date : + } ] + """ + query = """ + SELECT DISTINCT SFV.*, ue.ue_code + FROM + notes_ue ue, + notes_formations nf, + notes_formations nf2, + scolar_formsemestre_validation SFV, + notes_formsemestre sem, + notes_formsemestre_inscription ins + + WHERE ue.formation_id = nf.id + and nf.formation_code = nf2.formation_code + and nf2.id=%(formation_id)s + and ins.etudid = SFV.etudid + and ins.formsemestre_id = %(formsemestre_id)s + + and SFV.ue_id = ue.id + and SFV.code = 'ADM' + + and ( (sem.id = SFV.formsemestre_id + and sem.date_debut < %(date_debut)s + and sem.semestre_id = %(semestre_id)s ) + or ( + ((SFV.formsemestre_id is NULL) OR (SFV.is_external)) -- les UE externes ou "anterieures" + AND (SFV.semestre_id is NULL OR SFV.semestre_id=%(semestre_id)s) + ) ) + """ + params = { + "formation_id": formsemestre.formation.id, + "formsemestre_id": formsemestre.id, + "semestre_id": formsemestre.semestre_id, + "date_debut": formsemestre.date_debut, + } + + df = pd.read_sql_query(query, db.engine, params=params, index_col="etudid") + return df diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py index f890b6a61..f30f04912 100644 --- a/app/comp/moy_mod.py +++ b/app/comp/moy_mod.py @@ -77,6 +77,8 @@ class ModuleImplResults: "{ evaluation.id : bool } indique si à prendre en compte ou non." self.evaluations_etat = {} "{ evaluation_id: EvaluationEtat }" + self.en_attente = False + "Vrai si au moins une évaluation a une note en attente" # self.evals_notes = None """DataFrame, colonnes: EVALS, Lignes: etudid (inscrits au SEMESTRE) @@ -133,7 +135,7 @@ class ModuleImplResults: evals_notes = pd.DataFrame(index=self.etudids, dtype=float) self.evaluations_completes = [] self.evaluations_completes_dict = {} - + self.en_attente = False for evaluation in moduleimpl.evaluations: eval_df = self._load_evaluation_notes(evaluation) # is_complete ssi tous les inscrits (non dem) au semestre ont une note @@ -160,6 +162,8 @@ class ModuleImplResults: self.evaluations_etat[evaluation.id] = EvaluationEtat( evaluation_id=evaluation.id, nb_attente=nb_att, is_complete=is_complete ) + if nb_att > 0: + self.en_attente = True # Force columns names to integers (evaluation ids) evals_notes.columns = pd.Int64Index( @@ -209,6 +213,13 @@ class ModuleImplResults: * self.evaluations_completes ).reshape(-1, 1) + # was _list_notes_evals_titles + def get_evaluations_completes(self, moduleimpl: ModuleImpl) -> list: + "Liste des évaluations complètes" + return [ + e for e in moduleimpl.evaluations if self.evaluations_completes_dict[e.id] + ] + def get_eval_notes_sur_20(self, moduleimpl: ModuleImpl) -> np.array: """Les notes des évaluations, remplace les ATT, EXC, ABS, NaN par zéro et mets les notes sur 20. diff --git a/app/comp/res_cache.py b/app/comp/res_cache.py new file mode 100644 index 000000000..47c40b7e4 --- /dev/null +++ b/app/comp/res_cache.py @@ -0,0 +1,34 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +"""Cache pour résultats (super classe) +""" + +from app.models import FormSemestre + + +class ResultatsCache: + _cached_attrs = () # virtual + + def __init__(self, formsemestre: FormSemestre, cache_class=None): + self.formsemestre: FormSemestre = formsemestre + self.cache_class = cache_class + + def load_cached(self) -> bool: + "Load cached dataframes, returns False si pas en cache" + data = self.cache_class.get(self.formsemestre.id) + if not data: + return False + for attr in self._cached_attrs: + setattr(self, attr, data[attr]) + return True + + def store(self): + "Cache our data" + self.cache_class.set( + self.formsemestre.id, + {attr: getattr(self, attr) for attr in self._cached_attrs}, + ) diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 601f76a99..3c408257d 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -9,10 +9,13 @@ from functools import cached_property import numpy as np import pandas as pd from app.comp.aux_stats import StatsMoyenne +from app.comp.res_cache import ResultatsCache +from app.comp import res_sem from app.comp.moy_mod import ModuleImplResults from app.models import FormSemestre, Identite, ModuleImpl from app.models.ues import UniteEns from app.scodoc import sco_utils as scu +from app.scodoc import sco_evaluations from app.scodoc.sco_cache import ResultatsSemestreCache from app.scodoc.sco_codes_parcours import UE_SPORT, ATT, DEF @@ -25,7 +28,7 @@ from app.scodoc.sco_codes_parcours import UE_SPORT, ATT, DEF # (durée de vie de l'instance de ResultatsSemestre) # qui sont notamment les attributs décorés par `@cached_property`` # -class ResultatsSemestre: +class ResultatsSemestre(ResultatsCache): _cached_attrs = ( "etud_moy_gen_ranks", "etud_moy_gen", @@ -36,7 +39,7 @@ class ResultatsSemestre: ) def __init__(self, formsemestre: FormSemestre): - self.formsemestre: FormSemestre = formsemestre + super().__init__(formsemestre, ResultatsSemestreCache) # BUT ou standard ? (apc == "approche par compétences") self.is_apc = formsemestre.formation.is_apc() # Attributs "virtuels", définis dans les sous-classes @@ -46,26 +49,9 @@ class ResultatsSemestre: self.etud_moy_gen = {} self.etud_moy_gen_ranks = {} self.modimpls_results: ModuleImplResults = None + "Résultats de chaque modimpl: dict { modimpl.id : ModuleImplResults(Classique ou BUT) }" self.etud_coef_ue_df = None - """coefs d'UE effectifs pour chaque etudiant (pour form. classiques)""" - - # TODO ? - - def load_cached(self) -> bool: - "Load cached dataframes, returns False si pas en cache" - data = ResultatsSemestreCache.get(self.formsemestre.id) - if not data: - return False - for attr in self._cached_attrs: - setattr(self, attr, data[attr]) - return True - - def store(self): - "Cache our data" - ResultatsSemestreCache.set( - self.formsemestre.id, - {attr: getattr(self, attr) for attr in self._cached_attrs}, - ) + """coefs d'UE effectifs pour chaque étudiant (pour form. classiques)""" def compute(self): "Charge les notes et inscriptions et calcule toutes les moyennes" @@ -101,7 +87,8 @@ class ResultatsSemestre: @cached_property def ues(self) -> list[UniteEns]: """Liste des UEs du semestre (avec les UE bonus sport) - (indices des DataFrames) + (indices des DataFrames). + Note: un étudiant n'est pas nécessairement inscrit dans toutes ces UEs. """ return self.formsemestre.query_ues(with_sport=True).all() @@ -123,15 +110,34 @@ class ResultatsSemestre: if m.module.module_type == scu.ModuleType.SAE ] - @cached_property - def ue_validables(self) -> list: - """Liste des UE du semestre qui doivent être validées - (toutes sauf le sport) - """ - return self.formsemestre.query_ues().filter(UniteEns.type != UE_SPORT).all() + def get_etud_ue_validables(self, etudid: int) -> list[UniteEns]: + """Liste des UEs du semestre qui doivent être validées - def modimpls_in_ue(self, ue_id, etudid): - """Liste des modimpl de cet ue auxquels l'étudiant est inscrit""" + Rappel: l'étudiant est inscrit à des modimpls et non à des UEs. + + - En BUT: on considère que l'étudiant va (ou non) valider toutes les UEs des modules + du parcours. XXX notion à implémenter, pour l'instant toutes les UE du semestre. + + - En classique: toutes les UEs des modimpls auxquels l'étufdiant est inscrit sont + susceptibles d'être validées. + + Les UE "bonus" (sport) ne sont jamais "validables". + """ + if self.is_apc: + # TODO: introduire la notion de parcours (#sco93) + return self.formsemestre.query_ues().filter(UniteEns.type != UE_SPORT).all() + else: + # restreint aux UE auxquelles l'étudiant est inscrit (dans l'un des modimpls) + ues = { + modimpl.module.ue + for modimpl in self.formsemestre.modimpls_sorted + if self.modimpl_inscr_df[modimpl.id][etudid] + } + ues = sorted(list(ues), key=lambda x: x.numero or 0) + return ues + + def modimpls_in_ue(self, ue_id, etudid) -> list[ModuleImpl]: + """Liste des modimpl de cette UE auxquels l'étudiant est inscrit""" # sert pour l'affichage ou non de l'UE sur le bulletin return [ modimpl @@ -180,6 +186,7 @@ class NotesTableCompat(ResultatsSemestre): self.moy_moy = "NA" self.expr_diagnostics = "" self.parcours = self.formsemestre.formation.get_parcours() + self.validations = None def get_etudids(self, sorted=False) -> list[int]: """Liste des etudids inscrits, incluant les démissionnaires. @@ -243,6 +250,21 @@ class NotesTableCompat(ResultatsSemestre): modimpls_dict.append(d) return modimpls_dict + def get_etud_decision_ues(self, etudid: int) -> dict: + """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: + if not self.validations: + self.validations = res_sem.load_formsemestre_validations( + self.formsemestre + ) + return self.validations.decisions_jury_ues.get(etudid, None) + def get_etud_decision_sem(self, etudid: int) -> dict: """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 } @@ -256,12 +278,11 @@ class NotesTableCompat(ResultatsSemestre): "compense_formsemestre_id": None, } else: - return { - "code": ATT, # XXX TODO - "assidu": True, # XXX TODO - "event_date": "", - "compense_formsemestre_id": None, - } + if not self.validations: + self.validations = res_sem.load_formsemestre_validations( + self.formsemestre + ) + return self.validations.decisions_jury.get(etudid, None) def get_etud_etat(self, etudid: int) -> str: "Etat de l'etudiant: 'I', 'D', DEF ou '' (si pas connu dans ce semestre)" @@ -290,6 +311,31 @@ class NotesTableCompat(ResultatsSemestre): """ return self.etud_moy_gen[etudid] + def get_etud_ects_pot(self, etudid: int) -> dict: + """ + Un dict avec les champs + 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) + + Ce sont les ECTS des UE au dessus de la barre (10/20 en principe), avant le jury (donc non + encore enregistrées). + """ + # was nt.get_etud_moy_infos + # XXX pour compat nt, à remplacer ultérieurement + ues = self.get_etud_ue_validables(etudid) + ects_pot = 0.0 + for ue in ues: + if ( + ue.id in self.etud_moy_ue + and ue.ects is not None + and self.etud_moy_ue[ue.id][etudid] > self.parcours.NOTES_BARRE_VALID_UE + ): + ects_pot += ue.ects + return { + "ects_pot": ects_pot, + "ects_fond": 0.0, # not implemented (anciennemment pour école ingé) + } + def get_etud_ue_status(self, etudid: int, ue_id: int): coef_ue = self.etud_coef_ue_df[ue_id][etudid] return { @@ -333,8 +379,32 @@ class NotesTableCompat(ResultatsSemestre): evals_results.append(d) return evals_results + def get_evaluations_etats(self): + """[ {...evaluation et son etat...} ]""" + # TODO: à moderniser + if not hasattr(self, "_evaluations_etats"): + 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 états des évaluations de ce module""" + # XXX TODO à moderniser: lent, recharge des donénes que l'on a déjà... + return [ + e + for e in self.get_evaluations_etats() + if e["moduleimpl_id"] == moduleimpl_id + ] + def get_moduleimpls_attente(self): - return [] # XXX TODO + """Liste des modimpls du semestre ayant des notes en attente""" + return [ + modimpl + for modimpl in self.formsemestre.modimpls_sorted + if self.modimpls_results[modimpl.id].en_attente + ] def get_mod_stats(self, moduleimpl_id: int) -> dict: """Stats sur les notes obtenues dans un modimpl diff --git a/app/comp/res_sem.py b/app/comp/res_sem.py index 8e14d5afe..5da2c7f06 100644 --- a/app/comp/res_sem.py +++ b/app/comp/res_sem.py @@ -8,31 +8,49 @@ """ from flask import g +from app.comp.jury import ValidationsSemestre from app.comp.res_common import ResultatsSemestre from app.comp.res_classic import ResultatsSemestreClassic from app.comp.res_but import ResultatsSemestreBUT from app.models.formsemestre import FormSemestre -def load_formsemestre_result(formsemestre: FormSemestre) -> ResultatsSemestre: +def load_formsemestre_results(formsemestre: FormSemestre) -> ResultatsSemestre: """Returns ResultatsSemestre for this formsemestre. Suivant le type de formation, retour une instance de ResultatsSemestreClassic ou de ResultatsSemestreBUT. Search in local cache (g.formsemestre_result_cache) - then global app cache (eg REDIS) If not in cache, build it and cache it. """ # --- Try local cache (within the same request context) - if not hasattr(g, "formsemestre_result_cache"): - g.formsemestre_result_cache = {} # pylint: disable=C0237 + if not hasattr(g, "formsemestre_results_cache"): + g.formsemestre_results_cache = {} # pylint: disable=C0237 else: - if formsemestre.id in g.formsemestre_result_cache: - return g.formsemestre_result_cache[formsemestre.id] + if formsemestre.id in g.formsemestre_results_cache: + return g.formsemestre_results_cache[formsemestre.id] klass = ( ResultatsSemestreBUT if formsemestre.formation.is_apc() else ResultatsSemestreClassic ) - return klass(formsemestre) + g.formsemestre_results_cache[formsemestre.id] = klass(formsemestre) + return g.formsemestre_results_cache[formsemestre.id] + + +def load_formsemestre_validations(formsemestre: FormSemestre) -> ValidationsSemestre: + """Charge les résultats de jury de ce semestre. + Search in local cache (g.formsemestre_result_cache) + If not in cache, build it and cache it. + """ + if not hasattr(g, "formsemestre_validation_cache"): + g.formsemestre_validations_cache = {} # pylint: disable=C0237 + else: + if formsemestre.id in g.formsemestre_validations_cache: + return g.formsemestre_validations_cache[formsemestre.id] + + g.formsemestre_validations_cache[formsemestre.id] = ValidationsSemestre( + formsemestre + ) + return g.formsemestre_validations_cache[formsemestre.id] diff --git a/app/models/__init__.py b/app/models/__init__.py index f11084934..d29b6bf3b 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -49,13 +49,15 @@ from app.models.evaluations import ( ) from app.models.groups import Partition, GroupDescr, group_membership from app.models.notes import ( - ScolarEvent, - ScolarFormSemestreValidation, - ScolarAutorisationInscription, BulAppreciations, NotesNotes, NotesNotesLog, ) +from app.models.validations import ( + ScolarEvent, + ScolarFormSemestreValidation, + ScolarAutorisationInscription, +) from app.models.preferences import ScoPreference from app.models.but_refcomp import ( diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 2234fdfa1..e4b8fc8c8 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -158,7 +158,7 @@ class FormSemestre(db.Model): @cached_property def modimpls_sorted(self) -> list[ModuleImpl]: - """Liste des modimpls du semestre + """Liste des modimpls du semestre (y compris bonus) - triée par type/numéro/code en APC - triée par numéros d'UE/matières/modules pour les formations standard. """ diff --git a/app/models/notes.py b/app/models/notes.py index fa8dc8d10..7e5583579 100644 --- a/app/models/notes.py +++ b/app/models/notes.py @@ -8,100 +8,6 @@ from app.models import SHORT_STR_LEN from app.models import CODE_STR_LEN -class ScolarEvent(db.Model): - """Evenement dans le parcours scolaire d'un étudiant""" - - __tablename__ = "scolar_events" - id = db.Column(db.Integer, primary_key=True) - event_id = db.synonym("id") - etudid = db.Column( - db.Integer, - db.ForeignKey("identite.id"), - ) - event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) - formsemestre_id = db.Column( - db.Integer, - db.ForeignKey("notes_formsemestre.id"), - ) - ue_id = db.Column( - db.Integer, - db.ForeignKey("notes_ue.id"), - ) - # 'CREATION', 'INSCRIPTION', 'DEMISSION', - # 'AUT_RED', 'EXCLUS', 'VALID_UE', 'VALID_SEM' - # 'ECHEC_SEM' - # 'UTIL_COMPENSATION' - event_type = db.Column(db.String(SHORT_STR_LEN)) - # Semestre compensé par formsemestre_id: - comp_formsemestre_id = db.Column( - db.Integer, - db.ForeignKey("notes_formsemestre.id"), - ) - - -class ScolarFormSemestreValidation(db.Model): - """Décisions de jury""" - - __tablename__ = "scolar_formsemestre_validation" - # Assure unicité de la décision: - __table_args__ = (db.UniqueConstraint("etudid", "formsemestre_id", "ue_id"),) - - id = db.Column(db.Integer, primary_key=True) - formsemestre_validation_id = db.synonym("id") - etudid = db.Column( - db.Integer, - db.ForeignKey("identite.id"), - index=True, - ) - formsemestre_id = db.Column( - db.Integer, - db.ForeignKey("notes_formsemestre.id"), - index=True, - ) - ue_id = db.Column( - db.Integer, - db.ForeignKey("notes_ue.id"), - index=True, - ) - code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True) - # NULL pour les UE, True|False pour les semestres: - assidu = db.Column(db.Boolean) - event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) - # NULL sauf si compense un semestre: - compense_formsemestre_id = db.Column( - db.Integer, - db.ForeignKey("notes_formsemestre.id"), - ) - moy_ue = db.Column(db.Float) - # (normalement NULL) indice du semestre, utile seulement pour - # UE "antérieures" et si la formation définit des UE utilisées - # dans plusieurs semestres (cas R&T IUTV v2) - semestre_id = db.Column(db.Integer) - # Si UE validée dans le cursus d'un autre etablissement - is_external = db.Column(db.Boolean, default=False, server_default="false") - - -class ScolarAutorisationInscription(db.Model): - """Autorisation d'inscription dans un semestre""" - - __tablename__ = "scolar_autorisation_inscription" - id = db.Column(db.Integer, primary_key=True) - autorisation_inscription_id = db.synonym("id") - - etudid = db.Column( - db.Integer, - db.ForeignKey("identite.id"), - ) - formation_code = db.Column(db.String(SHORT_STR_LEN), nullable=False) - # semestre ou on peut s'inscrire: - semestre_id = db.Column(db.Integer) - date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) - origin_formsemestre_id = db.Column( - db.Integer, - db.ForeignKey("notes_formsemestre.id"), - ) - - class BulAppreciations(db.Model): """Appréciations sur bulletins""" diff --git a/app/models/validations.py b/app/models/validations.py new file mode 100644 index 000000000..0bf487f3a --- /dev/null +++ b/app/models/validations.py @@ -0,0 +1,109 @@ +# -*- coding: UTF-8 -* + +"""Notes, décisions de jury, évènements scolaires +""" + +from app import db +from app.models import SHORT_STR_LEN +from app.models import CODE_STR_LEN + + +class ScolarFormSemestreValidation(db.Model): + """Décisions de jury""" + + __tablename__ = "scolar_formsemestre_validation" + # Assure unicité de la décision: + __table_args__ = (db.UniqueConstraint("etudid", "formsemestre_id", "ue_id"),) + + id = db.Column(db.Integer, primary_key=True) + formsemestre_validation_id = db.synonym("id") + etudid = db.Column( + db.Integer, + db.ForeignKey("identite.id"), + index=True, + ) + formsemestre_id = db.Column( + db.Integer, + db.ForeignKey("notes_formsemestre.id"), + index=True, + ) + ue_id = db.Column( + db.Integer, + db.ForeignKey("notes_ue.id"), + index=True, + ) + code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True) + # NULL pour les UE, True|False pour les semestres: + assidu = db.Column(db.Boolean) + event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) + # NULL sauf si compense un semestre: + compense_formsemestre_id = db.Column( + db.Integer, + db.ForeignKey("notes_formsemestre.id"), + ) + moy_ue = db.Column(db.Float) + # (normalement NULL) indice du semestre, utile seulement pour + # UE "antérieures" et si la formation définit des UE utilisées + # dans plusieurs semestres (cas R&T IUTV v2) + semestre_id = db.Column(db.Integer) + # Si UE validée dans le cursus d'un autre etablissement + is_external = db.Column( + db.Boolean, default=False, server_default="false", index=True + ) + + ue = db.relationship("UniteEns", lazy="select", uselist=False) + + def __repr__(self): + return f"{self.__class__.__name__}({self.formsemestre_id}, {self.etudid}, code={self.code}, ue={self.ue_id}, moy_ue={self.moy_ue})" + + +class ScolarAutorisationInscription(db.Model): + """Autorisation d'inscription dans un semestre""" + + __tablename__ = "scolar_autorisation_inscription" + id = db.Column(db.Integer, primary_key=True) + autorisation_inscription_id = db.synonym("id") + + etudid = db.Column( + db.Integer, + db.ForeignKey("identite.id"), + ) + formation_code = db.Column(db.String(SHORT_STR_LEN), nullable=False) + # semestre ou on peut s'inscrire: + semestre_id = db.Column(db.Integer) + date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) + origin_formsemestre_id = db.Column( + db.Integer, + db.ForeignKey("notes_formsemestre.id"), + ) + + +class ScolarEvent(db.Model): + """Evenement dans le parcours scolaire d'un étudiant""" + + __tablename__ = "scolar_events" + id = db.Column(db.Integer, primary_key=True) + event_id = db.synonym("id") + etudid = db.Column( + db.Integer, + db.ForeignKey("identite.id"), + ) + event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) + formsemestre_id = db.Column( + db.Integer, + db.ForeignKey("notes_formsemestre.id"), + ) + ue_id = db.Column( + db.Integer, + db.ForeignKey("notes_ue.id"), + ) + # 'CREATION', 'INSCRIPTION', 'DEMISSION', + # 'AUT_RED', 'EXCLUS', 'VALID_UE', 'VALID_SEM' + # 'ECHEC_SEM' + # 'UTIL_COMPENSATION' + event_type = db.Column(db.String(SHORT_STR_LEN)) + # Semestre compensé par formsemestre_id: + comp_formsemestre_id = db.Column( + db.Integer, + db.ForeignKey("notes_formsemestre.id"), + ) diff --git a/app/scodoc/notes_table.py b/app/scodoc/notes_table.py index 07cbd1336..b9b4630ce 100644 --- a/app/scodoc/notes_table.py +++ b/app/scodoc/notes_table.py @@ -935,7 +935,7 @@ class NotesTable: """ return self.moy_gen[etudid] - def get_etud_moy_infos(self, etudid): + def get_etud_moy_infos(self, etudid): # XXX OBSOLETE """Infos sur moyennes""" return self.etud_moy_infos[etudid] @@ -1011,7 +1011,10 @@ class NotesTable: 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;", + """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 = {} @@ -1137,8 +1140,14 @@ class NotesTable: """ 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.sem, etudid) + 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 @@ -1308,7 +1317,7 @@ class NotesTable: """Liste des evaluations de ce semestre, avec leur etat""" return self.get_evaluations_etats() - def get_mod_evaluation_etat_list(self, moduleimpl_id): + def get_mod_evaluation_etat_list(self, moduleimpl_id) -> list[dict]: """Liste des évaluations de ce module""" return [ e diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index 23833f88c..d1127110c 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -142,7 +142,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"): prefs = sco_preferences.SemPreferences(formsemestre_id) # nt = sco_cache.NotesTableCache.get(formsemestre_id) # > toutes notes formsemestre = FormSemestre.query.get_or_404(formsemestre_id) - nt: NotesTableCompat = res_sem.load_formsemestre_result(formsemestre) + nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) if not nt.get_etud_etat(etudid): raise ScoValueError("Etudiant non inscrit à ce semestre") I = scu.DictDefault(defaultvalue="") diff --git a/app/scodoc/sco_cache.py b/app/scodoc/sco_cache.py index 59ebab2d5..5fd0cec68 100644 --- a/app/scodoc/sco_cache.py +++ b/app/scodoc/sco_cache.py @@ -59,9 +59,9 @@ import traceback from flask import g +from app import log from app.scodoc import notesdb as ndb from app.scodoc import sco_utils as scu -from app import log CACHE = None # set in app.__init__.py @@ -293,6 +293,7 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa SemBulletinsPDFCache.invalidate_sems(formsemestre_ids) ResultatsSemestreCache.delete_many(formsemestre_ids) + ValidationsSemestreCache.delete_many(formsemestre_ids) class DefferedSemCacheManager: @@ -319,10 +320,20 @@ class DefferedSemCacheManager: # ---- Nouvelles classes ScoDoc 9.2 class ResultatsSemestreCache(ScoDocCache): - """Cache pour les résultats ResultatsSemestre. + """Cache pour les résultats ResultatsSemestre (notes et moyennes) Clé: formsemestre_id Valeur: { un paquet de dataframes } """ prefix = "RSEM" timeout = 60 * 60 # ttl 1 heure (en phase de mise au point) + + +class ValidationsSemestreCache(ScoDocCache): + """Cache pour les résultats de jury d'un semestre + Clé: formsemestre_id + Valeur: un paquet de DataFrames + """ + + prefix = "VSC" + timeout = 60 * 60 # ttl 1 heure (en phase de mise au point) diff --git a/app/scodoc/sco_codes_parcours.py b/app/scodoc/sco_codes_parcours.py index 823dd19fc..59372b8d6 100644 --- a/app/scodoc/sco_codes_parcours.py +++ b/app/scodoc/sco_codes_parcours.py @@ -170,18 +170,18 @@ CODES_SEM_REO = {NAR: 1} # reorientation CODES_UE_VALIDES = {ADM: True, CMP: True} # UE validée -def code_semestre_validant(code): +def code_semestre_validant(code: str) -> bool: "Vrai si ce CODE entraine la validation du semestre" return CODES_SEM_VALIDES.get(code, False) -def code_semestre_attente(code): +def code_semestre_attente(code: str) -> bool: "Vrai si ce CODE est un code d'attente (semestre validable plus tard par jury ou compensation)" return CODES_SEM_ATTENTES.get(code, False) -def code_ue_validant(code): - "Vrai si ce code entraine la validation de l'UE" +def code_ue_validant(code: str) -> bool: + "Vrai si ce code entraine la validation des UEs du semestre." return CODES_UE_VALIDES.get(code, False) @@ -259,6 +259,7 @@ class TypeParcours(object): ) # par defaut, autorise tous les types d'UE APC_SAE = False # Approche par compétences avec ressources et SAÉs USE_REFERENTIEL_COMPETENCES = False # Lien avec ref. comp. + ECTS_FONDAMENTAUX_PER_YEAR = 0.0 # pour ISCID, deprecated def check(self, formation=None): return True, "" # status, diagnostic_message diff --git a/app/scodoc/sco_edit_apc.py b/app/scodoc/sco_edit_apc.py index c1c75319b..c6b0151d6 100644 --- a/app/scodoc/sco_edit_apc.py +++ b/app/scodoc/sco_edit_apc.py @@ -32,7 +32,7 @@ from flask_login import current_user from app import db from app.models import Formation, UniteEns, Matiere, Module, FormSemestre, ModuleImpl -from app.models.notes import ScolarFormSemestreValidation +from app.models.validations import ScolarFormSemestreValidation from app.scodoc.sco_codes_parcours import UE_SPORT import app.scodoc.sco_utils as scu from app.scodoc import sco_groups diff --git a/app/scodoc/sco_evaluations.py b/app/scodoc/sco_evaluations.py index 15b690a3c..c54806e23 100644 --- a/app/scodoc/sco_evaluations.py +++ b/app/scodoc/sco_evaluations.py @@ -393,9 +393,8 @@ def do_evaluation_etat_in_mod(nt, moduleimpl_id): """""" evals = nt.get_mod_evaluation_etat_list(moduleimpl_id) etat = _eval_etat(evals) - etat["attente"] = moduleimpl_id in [ - m["moduleimpl_id"] for m in nt.get_moduleimpls_attente() - ] # > liste moduleimpl en attente + # Il y a-t-il des notes en attente dans ce module ? + etat["attente"] = nt.modimpls_results[moduleimpl_id].en_attente return etat diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 1f440c8f2..fcab0e7d9 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -991,9 +991,9 @@ def formsemestre_status(formsemestre_id=None): modimpls = sco_moduleimpl.moduleimpl_withmodule_list( formsemestre_id=formsemestre_id ) - nt = sco_cache.NotesTableCache.get(formsemestre_id) - # WIP formsemestre = FormSemestre.query.get_or_404(formsemestre_id) - # WIP nt = res_sem.load_formsemestre_result(formsemestre) + # nt = sco_cache.NotesTableCache.get(formsemestre_id) + formsemestre = FormSemestre.query.get_or_404(formsemestre_id) + nt = res_sem.load_formsemestre_results(formsemestre) # Construit la liste de tous les enseignants de ce semestre: mails_enseignants = set( diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py index ed5325ce8..e835f1dd8 100644 --- a/app/scodoc/sco_formsemestre_validation.py +++ b/app/scodoc/sco_formsemestre_validation.py @@ -549,7 +549,7 @@ def formsemestre_recap_parcours_table( ass = "" formsemestre = FormSemestre.query.get(sem["formsemestre_id"]) - nt: NotesTableCompat = res_sem.load_formsemestre_result(formsemestre) + nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) # nt = sco_cache.NotesTableCache.get(sem["formsemestre_id"]) if is_cur: type_sem = "*" # now unused @@ -692,7 +692,7 @@ def formsemestre_recap_parcours_table( sco_preferences.get_preference("bul_show_ects", sem["formsemestre_id"]) or nt.parcours.ECTS_ONLY ): - etud_moy_infos = nt.get_etud_moy_infos(etudid) + etud_ects_infos = nt.get_etud_ects_pot(etudid) H.append( '' % (class_sem, sem["formsemestre_id"]) ) @@ -703,7 +703,7 @@ def formsemestre_recap_parcours_table( # total ECTS (affiché sous la moyenne générale) H.append( 'ECTS:%g %g' - % (etud_moy_infos["ects_pot"], etud_moy_infos["ects_pot_fond"]) + % (etud_ects_infos["ects_pot"], etud_ects_infos["ects_pot_fond"]) ) H.append('') # ECTS validables dans chaque UE @@ -1062,7 +1062,7 @@ def formsemestre_validate_previous_ue(formsemestre_id, etudid): "title": "Indice du semestre", "explanation": "Facultatif: indice du semestre dans la formation", "allow_null": True, - "allowed_values": [""] + [str(x) for x in range(11)], + "allowed_values": [""] + [x for x in range(11)], "labels": ["-"] + list(range(11)), }, ), diff --git a/app/scodoc/sco_liste_notes.py b/app/scodoc/sco_liste_notes.py index a342e3126..5d2871af6 100644 --- a/app/scodoc/sco_liste_notes.py +++ b/app/scodoc/sco_liste_notes.py @@ -837,7 +837,7 @@ def _add_apc_columns( # => On recharge tout dans les nouveaux modèles # rows est une liste de dict avec une clé "etudid" # on va y ajouter une clé par UE du semestre - nt: NotesTableCompat = res_sem.load_formsemestre_result(modimpl.formsemestre) + nt: NotesTableCompat = res_sem.load_formsemestre_results(modimpl.formsemestre) modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl.id] # XXX A ENLEVER TODO diff --git a/app/scodoc/sco_parcours_dut.py b/app/scodoc/sco_parcours_dut.py index 49c78263a..0d682c017 100644 --- a/app/scodoc/sco_parcours_dut.py +++ b/app/scodoc/sco_parcours_dut.py @@ -28,6 +28,7 @@ """Semestres: gestion parcours DUT (Arreté du 13 août 2005) """ +from app.models.ues import UniteEns import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app import log @@ -678,10 +679,10 @@ class SituationEtudParcoursECTS(SituationEtudParcoursGeneric): Dans ce type de parcours, on n'utilise que ADM, AJ, et ADJ (?). """ - etud_moy_infos = self.nt.get_etud_moy_infos(self.etudid) + etud_ects_infos = self.nt.get_etud_ects_pot(self.etudid) if ( - etud_moy_infos["ects_pot"] >= self.parcours.ECTS_BARRE_VALID_YEAR - and etud_moy_infos["ects_pot"] >= self.parcours.ECTS_FONDAMENTAUX_PER_YEAR + etud_ects_infos["ects_pot"] >= self.parcours.ECTS_BARRE_VALID_YEAR + and etud_ects_infos["ects_pot"] >= self.parcours.ECTS_FONDAMENTAUX_PER_YEAR ): choices = [ DecisionSem( @@ -954,6 +955,9 @@ def do_formsemestre_validate_ue( is_external=False, ): """Ajoute ou change validation UE""" + if semestre_id is None: + ue = UniteEns.query.get_or_404(ue_id) + semestre_id = ue.semestre_idx args = { "formsemestre_id": formsemestre_id, "etudid": etudid, @@ -971,7 +975,8 @@ def do_formsemestre_validate_ue( if formsemestre_id: cond += " and formsemestre_id=%(formsemestre_id)s" if semestre_id: - cond += " and semestre_id=%(semestre_id)s" + cond += " and (semestre_id=%(semestre_id)s or semestre_id is NULL)" + log(f"formsemestre_validate_ue: deleting where {cond}, args={args})") cursor.execute("delete from scolar_formsemestre_validation where " + cond, args) # insert args["code"] = code @@ -980,7 +985,7 @@ def do_formsemestre_validate_ue( # stocke la moyenne d'UE capitalisée: moy_ue = nt.get_etud_ue_status(etudid, ue_id)["moy"] args["moy_ue"] = moy_ue - log("formsemestre_validate_ue: %s" % args) + log("formsemestre_validate_ue: create %s" % args) if code != None: scolar_formsemestre_validation_create(cnx, args) else: @@ -1039,7 +1044,9 @@ def formsemestre_get_autorisation_inscription(etudid, origin_formsemestre_id): ) -def formsemestre_get_etud_capitalisation(sem, etudid): +def formsemestre_get_etud_capitalisation( + formation_id: int, semestre_idx: int, date_debut, etudid: int +) -> list[dict]: """Liste des UE capitalisées (ADM) correspondant au semestre sem et à l'étudiant. Recherche dans les semestres de la même formation (code) avec le même @@ -1057,30 +1064,32 @@ def formsemestre_get_etud_capitalisation(sem, etudid): cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor.execute( - """select distinct SFV.*, ue.ue_code from notes_ue ue, notes_formations nf, - notes_formations nf2, scolar_formsemestre_validation SFV, notes_formsemestre sem + """ + SELECT DISTINCT SFV.*, ue.ue_code + FROM notes_ue ue, notes_formations nf, + notes_formations nf2, scolar_formsemestre_validation SFV, notes_formsemestre sem - WHERE ue.formation_id = nf.id + WHERE ue.formation_id = nf.id and nf.formation_code = nf2.formation_code and nf2.id=%(formation_id)s and SFV.ue_id = ue.id and SFV.code = 'ADM' and SFV.etudid = %(etudid)s - - and ( (sem.id = SFV.formsemestre_id - and sem.date_debut < %(date_debut)s - and sem.semestre_id = %(semestre_id)s ) - or ( - ((SFV.formsemestre_id is NULL) OR (SFV.is_external)) -- les UE externes ou "anterieures" - AND (SFV.semestre_id is NULL OR SFV.semestre_id=%(semestre_id)s) + + and ( (sem.id = SFV.formsemestre_id + and sem.date_debut < %(date_debut)s + and sem.semestre_id = %(semestre_id)s ) + or ( + ((SFV.formsemestre_id is NULL) OR (SFV.is_external)) -- les UE externes ou "anterieures" + AND (SFV.semestre_id is NULL OR SFV.semestre_id=%(semestre_id)s) ) ) """, { "etudid": etudid, - "formation_id": sem["formation_id"], - "semestre_id": sem["semestre_id"], - "date_debut": ndb.DateDMYtoISO(sem["date_debut"]), + "formation_id": formation_id, + "semestre_id": semestre_idx, + "date_debut": date_debut, }, ) diff --git a/app/scodoc/sco_pvjury.py b/app/scodoc/sco_pvjury.py index ea43cb2d2..292c618a2 100644 --- a/app/scodoc/sco_pvjury.py +++ b/app/scodoc/sco_pvjury.py @@ -140,23 +140,23 @@ def descr_autorisations(autorisations): return ", ".join(alist) -def _comp_ects_by_ue_code_and_type(nt, decision_ues): +def _comp_ects_by_ue_code(nt, decision_ues): """Calcul somme des ECTS validés dans ce semestre (sans les UE capitalisées) decision_ues est le resultat de nt.get_etud_decision_ues Chaque resultat est un dict: { ue_code : ects } """ + raise NotImplementedError() # XXX #sco92 + # ré-écrire en utilisant if not decision_ues: - return {}, {} + return {} ects_by_ue_code = {} - ects_by_ue_type = scu.DictDefault(defaultvalue=0) # { ue_type : ects validés } for ue_id in decision_ues: d = decision_ues[ue_id] ue = nt.uedict[ue_id] ects_by_ue_code[ue["ue_code"]] = d["ects"] - ects_by_ue_type[ue["type"]] += d["ects"] - return ects_by_ue_code, ects_by_ue_type + return ects_by_ue_code def _comp_ects_capitalises_by_ue_code(nt, etudid): @@ -249,11 +249,8 @@ def dict_pvjury( ects_capitalises_by_ue_code = _comp_ects_capitalises_by_ue_code(nt, etudid) d["sum_ects_capitalises"] = sum(ects_capitalises_by_ue_code.values()) - ects_by_ue_code, ects_by_ue_type = _comp_ects_by_ue_code_and_type( - nt, d["decisions_ue"] - ) + ects_by_ue_code = _comp_ects_by_ue_code(nt, d["decisions_ue"]) d["sum_ects"] = _sum_ects_dicts(ects_capitalises_by_ue_code, ects_by_ue_code) - d["sum_ects_by_type"] = ects_by_ue_type if d["decision_sem"] and sco_codes_parcours.code_semestre_validant( d["decision_sem"]["code"] diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py index 997e835ea..87ab8c26a 100644 --- a/app/scodoc/sco_recapcomplet.py +++ b/app/scodoc/sco_recapcomplet.py @@ -25,7 +25,7 @@ # ############################################################################## -"""Tableau recapitulatif des notes d'un semestre +"""Tableau récapitulatif des notes d'un semestre """ import datetime import json @@ -41,6 +41,7 @@ from app.comp import res_sem from app.comp.res_common import NotesTableCompat from app.models import FormSemestre from app.models.etudiants import Identite +from app.models.evaluations import Evaluation import app.scodoc.sco_utils as scu from app.scodoc import html_sco_header @@ -308,8 +309,8 @@ def make_formsemestre_recapcomplet( # nt = sco_cache.NotesTableCache.get(formsemestre_id) # sco91 # sco92 : - nt: NotesTableCompat = res_sem.load_formsemestre_result(formsemestre) - modimpls = nt.get_modimpls_dict() + nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) + modimpls = formsemestre.modimpls_sorted ues = nt.get_ues_stat_dict() # incluant le(s) UE de sport # # if formsemestre.formation.is_apc(): @@ -367,15 +368,16 @@ def make_formsemestre_recapcomplet( pass if not hidemodules and not ue["is_external"]: for modimpl in modimpls: - if modimpl["module"]["ue_id"] == ue["ue_id"]: - code = modimpl["module"]["code"] + if modimpl.module.ue_id == ue["ue_id"]: + code = modimpl.module.code h.append(code) cod2mod[code] = modimpl # pour fabriquer le lien if format == "xlsall": - evals = nt.get_mod_evaluation_etat_list( - modimpl["moduleimpl_id"] - ) - mod_evals[modimpl["moduleimpl_id"]] = evals + evals = nt.modimpls_results[ + modimpl.id + ].get_evaluations_completes(modimpl) + # evals = nt.get_mod_evaluation_etat_list(... + mod_evals[modimpl.id] = evals h += _list_notes_evals_titles(code, evals) h += admission_extra_cols @@ -483,7 +485,7 @@ def make_formsemestre_recapcomplet( if not hidemodules and not ue["is_external"]: j = 0 for modimpl in modimpls: - if modimpl["module"]["ue_id"] == ue["ue_id"]: + if modimpl.module.ue_id == ue["ue_id"]: l.append( fmtnum( scu.fmt_note( @@ -492,9 +494,7 @@ def make_formsemestre_recapcomplet( ) ) # moyenne etud dans module if format == "xlsall": - l += _list_notes_evals( - mod_evals[modimpl["moduleimpl_id"]], etudid - ) + l += _list_notes_evals(mod_evals[modimpl.id], etudid) j += 1 if not hidebac: for k in admission_extra_cols: @@ -509,9 +509,7 @@ def make_formsemestre_recapcomplet( if not hidemodules: # moy/min/max dans chaque module mods_stats = {} # moduleimpl_id : stats for modimpl in modimpls: - mods_stats[modimpl["moduleimpl_id"]] = nt.get_mod_stats( - modimpl["moduleimpl_id"] - ) + mods_stats[modimpl.id] = nt.get_mod_stats(modimpl.id) def add_bottom_stat(key, title, corner_value=""): l = ["", title] @@ -551,16 +549,16 @@ def make_formsemestre_recapcomplet( # ue_index.append(len(l) - 1) if not hidemodules and not ue["is_external"]: for modimpl in modimpls: - if modimpl["module"]["ue_id"] == ue["ue_id"]: + if modimpl.module.ue_id == ue["ue_id"]: if key == "coef": - coef = modimpl["module"]["coefficient"] + coef = modimpl.module.coefficient if format[:3] != "xls": coef = str(coef) l.append(coef) elif key == "ects": l.append("") # ECTS module ? else: - val = mods_stats[modimpl["moduleimpl_id"]][key] + val = mods_stats[modimpl.id][key] if key == "nb_valid_evals": if ( format[:3] != "xls" @@ -571,9 +569,7 @@ def make_formsemestre_recapcomplet( l.append(val) if format == "xlsall": - l += _list_notes_evals_stats( - mod_evals[modimpl["moduleimpl_id"]], key - ) + l += _list_notes_evals_stats(mod_evals[modimpl.id], key) if modejury: l.append("") # case vide sur ligne "Moyennes" @@ -595,7 +591,7 @@ def make_formsemestre_recapcomplet( add_bottom_stat("nb_valid_evals", "Nb évals") add_bottom_stat("ects", "ECTS") - # Generation table au format demandé + # Génération de la table au format demandé if format == "html": # Table format HTML H = [ @@ -838,63 +834,50 @@ def make_formsemestre_recapcomplet( raise ValueError("unknown format %s" % format) -def _list_notes_evals(evals, etudid): +def _list_notes_evals(evals: list[Evaluation], etudid: int) -> list[str]: """Liste des notes des evaluations completes de ce module (pour table xls avec evals) """ L = [] for e in evals: - if ( - e["etat"]["evalcomplete"] - or e["etat"]["evalattente"] - or e["publish_incomplete"] - ): - notes_db = sco_evaluation_db.do_evaluation_get_all_notes(e["evaluation_id"]) - if etudid in notes_db: - val = notes_db[etudid]["value"] - else: - # Note manquante mais prise en compte immédiate: affiche ATT - val = scu.NOTES_ATTENTE - val_fmt = scu.fmt_note(val, keep_numeric=True) - L.append(val_fmt) + notes_db = sco_evaluation_db.do_evaluation_get_all_notes(e.evaluation_id) + if etudid in notes_db: + val = notes_db[etudid]["value"] + else: + # Note manquante mais prise en compte immédiate: affiche ATT + val = scu.NOTES_ATTENTE + val_fmt = scu.fmt_note(val, keep_numeric=True) + L.append(val_fmt) return L -def _list_notes_evals_titles(codemodule, evals): +def _list_notes_evals_titles(codemodule: str, evals: list[Evaluation]) -> list[str]: """Liste des titres des evals completes""" L = [] eval_index = len(evals) - 1 for e in evals: - if ( - e["etat"]["evalcomplete"] - or e["etat"]["evalattente"] - or e["publish_incomplete"] - ): - L.append(codemodule + "-" + str(eval_index) + "-" + e["jour"].isoformat()) + L.append(codemodule + "-" + str(eval_index) + "-" + e.jour.isoformat()) eval_index -= 1 return L -def _list_notes_evals_stats(evals, key): +def _list_notes_evals_stats(evals: list[Evaluation], key: str) -> list[str]: """Liste des stats (moy, ou rien!) des evals completes""" L = [] for e in evals: - if ( - e["etat"]["evalcomplete"] - or e["etat"]["evalattente"] - or e["publish_incomplete"] - ): - if key == "moy": - val = e["etat"]["moy_num"] - L.append(scu.fmt_note(val, keep_numeric=True)) - elif key == "max": - L.append(e["note_max"]) - elif key == "min": - L.append(0.0) - elif key == "coef": - L.append(e["coefficient"]) - else: - L.append("") # on n'a pas sous la main min/max + if key == "moy": + # TODO #sco92 + # val = e["etat"]["moy_num"] + # L.append(scu.fmt_note(val, keep_numeric=True)) + L.append("") + elif key == "max": + L.append(e.note_max) + elif key == "min": + L.append(0.0) + elif key == "coef": + L.append(e.coefficient) + else: + L.append("") # on n'a pas sous la main min/max return L diff --git a/app/views/notes.py b/app/views/notes.py index 4b299ee52..0762a5cee 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -2140,7 +2140,7 @@ def formsemestre_validation_etud_manu( ) -@bp.route("/formsemestre_validate_previous_ue") +@bp.route("/formsemestre_validate_previous_ue", methods=["GET", "POST"]) @scodoc @permission_required(Permission.ScoView) @scodoc7func From 39fa7f2487576c96a4a5ad5aea0e3c86f72b3b06 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 6 Feb 2022 18:29:22 +0100 Subject: [PATCH 6/8] rangs dans les UEs --- app/but/bulletin_but.py | 3 +++ app/comp/res_but.py | 3 ++- app/comp/res_classic.py | 4 ++-- app/comp/res_common.py | 17 +++++++++++++++++ app/scodoc/sco_pvjury.py | 7 +++---- app/scodoc/sco_recapcomplet.py | 8 ++++---- 6 files changed, 31 insertions(+), 11 deletions(-) diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index db05abfd4..efa900eb0 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -77,11 +77,14 @@ class BulletinBUT(ResultatsSemestreBUT): "saes": self.etud_ue_mod_results(etud, ue, self.saes), } if ue.type != UE_SPORT: + rangs, effectif = self.ue_rangs[ue.id] d["moyenne"] = { "value": fmt_note(self.etud_moy_ue[ue.id][etud.id]), "min": fmt_note(self.etud_moy_ue[ue.id].min()), "max": fmt_note(self.etud_moy_ue[ue.id].max()), "moy": fmt_note(self.etud_moy_ue[ue.id].mean()), + "rang": rangs[etud.id], + "total": effectif, # nb etud avec note dans cette UE } else: # ceci suppose que l'on a une seule UE bonus, diff --git a/app/comp/res_but.py b/app/comp/res_but.py index c4697238d..b9be879b0 100644 --- a/app/comp/res_but.py +++ b/app/comp/res_but.py @@ -98,7 +98,8 @@ class ResultatsSemestreBUT(NotesTableCompat): self.etud_moy_gen = moy_sem.compute_sem_moys_apc( self.etud_moy_ue, self.modimpl_coefs_df ) - self.etud_moy_gen_ranks = moy_sem.comp_ranks_series(self.etud_moy_gen) + # --- Classements: + self.compute_rangs() def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float: """La moyenne de l'étudiant dans le moduleimpl diff --git a/app/comp/res_classic.py b/app/comp/res_classic.py index 58c505ee5..9fdb1be29 100644 --- a/app/comp/res_classic.py +++ b/app/comp/res_classic.py @@ -9,7 +9,7 @@ import numpy as np import pandas as pd -from app.comp import moy_mod, moy_ue, moy_sem, inscr_mod +from app.comp import moy_mod, moy_ue, inscr_mod from app.comp.res_common import NotesTableCompat from app.comp.bonus_spo import BonusSport from app.models import ScoDocSiteConfig @@ -105,7 +105,7 @@ class ResultatsSemestreClassic(NotesTableCompat): bonus_mg # compat nt, utilisé pour l'afficher sur les bulletins ) # --- Classements: - self.etud_moy_gen_ranks = moy_sem.comp_ranks_series(self.etud_moy_gen) + self.compute_rangs() def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float: """La moyenne de l'étudiant dans le moduleimpl diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 3c408257d..69300d998 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -9,6 +9,7 @@ from functools import cached_property import numpy as np import pandas as pd from app.comp.aux_stats import StatsMoyenne +from app.comp import moy_sem from app.comp.res_cache import ResultatsCache from app.comp import res_sem from app.comp.moy_mod import ModuleImplResults @@ -169,6 +170,8 @@ class NotesTableCompat(ResultatsSemestre): "bonus", "bonus_ues", "malus", + "etud_moy_gen_ranks", + "ue_rangs", ) def __init__(self, formsemestre: FormSemestre): @@ -250,6 +253,20 @@ class NotesTableCompat(ResultatsSemestre): modimpls_dict.append(d) return modimpls_dict + def compute_rangs(self): + """Calcule les classements + Moyenne générale: etud_moy_gen_ranks + Par UE: + """ + self.etud_moy_gen_ranks = moy_sem.comp_ranks_series(self.etud_moy_gen) + for ue in self.formsemestre.query_ues(): + moy_ue = self.etud_moy_ue[ue.id] + self.ue_rangs[ue.id] = ( + moy_sem.comp_ranks_series(moy_ue), + int(moy_ue.count()), + ) + # .count() -> nb of non NaN values + def get_etud_decision_ues(self, etudid: int) -> dict: """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. diff --git a/app/scodoc/sco_pvjury.py b/app/scodoc/sco_pvjury.py index 292c618a2..ecf2d4887 100644 --- a/app/scodoc/sco_pvjury.py +++ b/app/scodoc/sco_pvjury.py @@ -53,6 +53,7 @@ from reportlab.lib import styles import flask from flask import url_for, g, request +from app.models.ues import UniteEns import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb @@ -145,16 +146,14 @@ def _comp_ects_by_ue_code(nt, decision_ues): decision_ues est le resultat de nt.get_etud_decision_ues Chaque resultat est un dict: { ue_code : ects } """ - raise NotImplementedError() # XXX #sco92 - # ré-écrire en utilisant if not decision_ues: return {} ects_by_ue_code = {} for ue_id in decision_ues: d = decision_ues[ue_id] - ue = nt.uedict[ue_id] - ects_by_ue_code[ue["ue_code"]] = d["ects"] + ue = UniteEns.query.get(ue_id) + ects_by_ue_code[ue.ue_code] = d["ects"] return ects_by_ue_code diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py index 87ab8c26a..16830b081 100644 --- a/app/scodoc/sco_recapcomplet.py +++ b/app/scodoc/sco_recapcomplet.py @@ -645,12 +645,12 @@ def make_formsemestre_recapcomplet( ): # Rang: force tri numerique pour sortable cls = cls + " sortnumeric" if F[0][i] in cod2mod: # lien vers etat module - mod = cod2mod[F[0][i]] + modimpl = cod2mod[F[0][i]] cells += '%s' % ( cls, - mod["moduleimpl_id"], - mod["module"]["titre"], - sco_users.user_info(mod["responsable_id"])["nomcomplet"], + modimpl.id, + modimpl.module.titre, + sco_users.user_info(modimpl.responsable_id)["nomcomplet"], F[0][i], ) else: From 844a90ed621def8a3fe0d9195cfc8c7793bdd949 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 6 Feb 2022 18:40:00 +0100 Subject: [PATCH 7/8] =?UTF-8?q?N'exporte=20pas=20les=20rangs=20d'UE=20si?= =?UTF-8?q?=20option=20d=C3=A9sactiv=C3=A9e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/bulletin_but.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index efa900eb0..9a886efff 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -77,13 +77,19 @@ class BulletinBUT(ResultatsSemestreBUT): "saes": self.etud_ue_mod_results(etud, ue, self.saes), } if ue.type != UE_SPORT: - rangs, effectif = self.ue_rangs[ue.id] + if sco_preferences.get_preference( + "bul_show_ue_rangs", self.formsemestre.id + ): + rangs, effectif = self.ue_rangs[ue.id] + rang = rangs[etud.id] + else: + rang, effectif = "", 0 d["moyenne"] = { "value": fmt_note(self.etud_moy_ue[ue.id][etud.id]), "min": fmt_note(self.etud_moy_ue[ue.id].min()), "max": fmt_note(self.etud_moy_ue[ue.id].max()), "moy": fmt_note(self.etud_moy_ue[ue.id].mean()), - "rang": rangs[etud.id], + "rang": rang, "total": effectif, # nb etud avec note dans cette UE } else: From a9df233c2e4dd1901b408894a09ee0329ce1ba70 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 7 Feb 2022 16:32:04 +0100 Subject: [PATCH 8/8] =?UTF-8?q?WIP:=20prise=20en=20compte=20des=20UE=20cap?= =?UTF-8?q?italis=C3=A9es=20(!=20calculs=20erron=C3=A9s/en=20cours)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/comp/jury.py | 7 +++ app/comp/res_but.py | 15 ++++- app/comp/res_classic.py | 74 ++++++++++++++++++++++ app/comp/res_common.py | 126 +++++++++++++++++++++++++++++++++---- app/models/formsemestre.py | 2 + app/scodoc/notes_table.py | 3 +- app/scodoc/sco_pvjury.py | 5 +- 7 files changed, 215 insertions(+), 17 deletions(-) diff --git a/app/comp/jury.py b/app/comp/jury.py index bb7cd0d4e..6581a2fb0 100644 --- a/app/comp/jury.py +++ b/app/comp/jury.py @@ -34,6 +34,13 @@ class ValidationsSemestre(ResultatsCache): """Décisions sur des UEs dans ce semestre: { etudid : { ue_id : { 'code' : Note|ADM|CMP, 'event_date' }}} """ + self.ue_capitalisees: pd.DataFrame = None + """DataFrame, index etudid + formsemestre_id : origine de l'UE capitalisée + is_external : vrai si validation effectuée dans un semestre extérieur + ue_id : dans le semestre origine (pas toujours de la même formation) + ue_code : code de l'UE, moy_ue : note enregistrée, + event_date : date de la validation (jury).""" if not self.load_cached(): self.compute() diff --git a/app/comp/res_but.py b/app/comp/res_but.py index b9be879b0..c5267596a 100644 --- a/app/comp/res_but.py +++ b/app/comp/res_but.py @@ -12,6 +12,7 @@ from app.comp import moy_ue, moy_sem, inscr_mod from app.comp.res_common import NotesTableCompat from app.comp.bonus_spo import BonusSport from app.models import ScoDocSiteConfig +from app.models.ues import UniteEns from app.scodoc.sco_codes_parcours import UE_SPORT @@ -93,11 +94,14 @@ class ResultatsSemestreBUT(NotesTableCompat): self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True) # Moyenne générale indicative: - # (note: le bonus sport a déjà été appliqué aux moyenens d'UE, et impacte + # (note: le bonus sport a déjà été appliqué aux moyennes d'UE, et impacte # donc la moyenne indicative) self.etud_moy_gen = moy_sem.compute_sem_moys_apc( self.etud_moy_ue, self.modimpl_coefs_df ) + # --- UE capitalisées + self.apply_capitalisation() + # --- Classements: self.compute_rangs() @@ -110,3 +114,12 @@ class ResultatsSemestreBUT(NotesTableCompat): etud_idx = self.etud_index[etudid] # moyenne sur les UE: return self.sem_cube[etud_idx, mod_idx].mean() + + def compute_etud_ue_coef(self, etudid: int, ue: UniteEns) -> float: + """Détermine le coefficient de l'UE pour cet étudiant. + N'est utilisé que pour l'injection des UE capitalisées dans la + moyenne générale. + En BUT, c'est simple: Coef = somme des coefs des modules vers cette UE. + (ne dépend pas des modules auxquels est inscrit l'étudiant, ). + """ + return self.modimpl_coefs_df.loc[ue.id].sum() diff --git a/app/comp/res_classic.py b/app/comp/res_classic.py index 9fdb1be29..0d8f32423 100644 --- a/app/comp/res_classic.py +++ b/app/comp/res_classic.py @@ -6,15 +6,24 @@ """Résultats semestres classiques (non APC) """ + import numpy as np import pandas as pd +from sqlalchemy.sql import text +from flask import g, url_for + +from app import db +from app import log from app.comp import moy_mod, moy_ue, inscr_mod from app.comp.res_common import NotesTableCompat from app.comp.bonus_spo import BonusSport from app.models import ScoDocSiteConfig +from app.models.etudiants import Identite from app.models.formsemestre import FormSemestre +from app.models.ues import UniteEns from app.scodoc.sco_codes_parcours import UE_SPORT +from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_utils import ModuleType @@ -104,6 +113,9 @@ class ResultatsSemestreClassic(NotesTableCompat): self.bonus = ( bonus_mg # compat nt, utilisé pour l'afficher sur les bulletins ) + # --- UE capitalisées + self.apply_capitalisation() + # --- Classements: self.compute_rangs() @@ -132,6 +144,42 @@ class ResultatsSemestreClassic(NotesTableCompat): ), } + def compute_etud_ue_coef(self, etudid: int, ue: UniteEns) -> float: + """Détermine le coefficient de l'UE pour cet étudiant. + N'est utilisé que pour l'injection des UE capitalisées dans la + moyenne générale. + Coef = 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" + % (self.formsemestre.id, etudid, ue) + ) + etud: Identite = Identite.query.get(etudid) + 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), + etud.nom_disp(), + 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 notes_sem_load_matrix(formsemestre: FormSemestre) -> tuple[np.ndarray, dict]: """Calcule la matrice des notes du semestre @@ -165,3 +213,29 @@ def notes_sem_assemble_matrix(modimpls_notes: list[pd.Series]) -> np.ndarray: modimpls_notes = np.stack(modimpls_notes_arr) # passe de (mod x etud) à (etud x mod) return modimpls_notes.T + + +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. + """ + # comme l'ancien notes_table.comp_etud_sum_coef_modules_ue + # mais en raw sqlalchemy et la somme en SQL + sql = text( + """ + SELECT sum(mod.coefficient) + FROM notes_modules mod, notes_moduleimpl mi, notes_moduleimpl_inscription ins + WHERE mod.id = mi.module_id + and ins.etudid = :etudid + and ins.moduleimpl_id = mi.id + and mi.formsemestre_id = :formsemestre_id + and mod.ue_id = :ue_id + """ + ) + cursor = db.session.execute( + sql, {"etudid": etudid, "formsemestre_id": formsemestre_id, "ue_id": ue_id} + ) + r = cursor.fetchone() + if r is None: + return None + return r[0] diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 69300d998..09d3ed75f 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -8,17 +8,22 @@ from collections import defaultdict, Counter from functools import cached_property import numpy as np import pandas as pd + +from flask import g + from app.comp.aux_stats import StatsMoyenne from app.comp import moy_sem from app.comp.res_cache import ResultatsCache from app.comp import res_sem from app.comp.moy_mod import ModuleImplResults from app.models import FormSemestre, Identite, ModuleImpl +from app.models import FormSemestreUECoef from app.models.ues import UniteEns from app.scodoc import sco_utils as scu from app.scodoc import sco_evaluations from app.scodoc.sco_cache import ResultatsSemestreCache from app.scodoc.sco_codes_parcours import UE_SPORT, ATT, DEF +from app.scodoc.sco_exceptions import ScoValueError # Il faut bien distinguer # - ce qui est caché de façon persistente (via redis): @@ -53,6 +58,7 @@ class ResultatsSemestre(ResultatsCache): "Résultats de chaque modimpl: dict { modimpl.id : ModuleImplResults(Classique ou BUT) }" self.etud_coef_ue_df = None """coefs d'UE effectifs pour chaque étudiant (pour form. classiques)""" + self.validations = None def compute(self): "Charge les notes et inscriptions et calcule toutes les moyennes" @@ -155,6 +161,115 @@ class ResultatsSemestre(ResultatsCache): """ return self.etud_moy_ue > (seuil - scu.NOTES_TOLERANCE) + def apply_capitalisation(self): + """Recalcule la moyenne générale pour prendre en compte d'éventuelles + UE capitalisées. + """ + # Supposant qu'il y a peu d'UE capitalisées, + # on va soustraire la moyenne d'UE et ajouter celle de l'UE capitalisée. + # return # XXX XXX XXX + if not self.validations: + self.validations = res_sem.load_formsemestre_validations(self.formsemestre) + ue_capitalisees = self.validations.ue_capitalisees + ue_by_code = {} + for etudid in ue_capitalisees.index: + recompute_mg = False + # ue_codes = set(ue_capitalisees.loc[etudid]["ue_code"]) + # for ue_code in ue_codes: + # ue = ue_by_code.get(ue_code) + # if ue is None: + # ue = self.formsemestre.query_ues.filter_by(ue_code=ue_code) + # ue_by_code[ue_code] = ue + + # Quand il y a une capitalisation, vérifie toutes les UEs + sum_notes_ue = 0.0 + sum_coefs_ue = 0.0 + for ue in self.formsemestre.query_ues(): + ue_cap = self.get_etud_ue_status(etudid, ue.id) + if ue_cap["is_capitalized"]: + recompute_mg = True + coef = ue_cap["coef_ue"] + if not np.isnan(ue_cap["moy"]): + sum_notes_ue += ue_cap["moy"] * coef + sum_coefs_ue += coef + + if recompute_mg and sum_coefs_ue > 0.0: + # On doit prendre en compte une ou plusieurs UE capitalisées + # et donc recalculer la moyenne générale + self.etud_moy_gen[etudid] = sum_notes_ue / sum_coefs_ue + + def _get_etud_ue_cap(self, etudid, ue): + """""" + capitalisations = self.validations.ue_capitalisees.loc[etudid] + if isinstance(capitalisations, pd.DataFrame): + ue_cap = capitalisations[capitalisations["ue_code"] == ue.ue_code] + if isinstance(ue_cap, pd.DataFrame) and not ue_cap.empty: + # si plusieurs fois capitalisée, prend le max + cap_idx = ue_cap["moy_ue"].values.argmax() + ue_cap = ue_cap.iloc[cap_idx] + else: + if capitalisations["ue_code"] == ue.ue_code: + ue_cap = capitalisations + else: + ue_cap = None + return ue_cap + + def get_etud_ue_status(self, etudid: int, ue_id: int) -> dict: + """L'état de l'UE pour cet étudiant. + Result: dict + """ + if not self.validations: + self.validations = res_sem.load_formsemestre_validations(self.formsemestre) + ue = UniteEns.query.get(ue_id) # TODO cacher nos UEs ? + cur_moy_ue = self.etud_moy_ue[ue_id][etudid] + moy_ue = cur_moy_ue + is_capitalized = False + if etudid in self.validations.ue_capitalisees.index: + ue_cap = self._get_etud_ue_cap(etudid, ue) + if ue_cap is not None and not ue_cap.empty: + if ue_cap["moy_ue"] > cur_moy_ue: + moy_ue = ue_cap["moy_ue"] + is_capitalized = True + if is_capitalized: + coef_ue = 1.0 + coef_ue = self.etud_coef_ue_df[ue_id][etudid] + + return { + "is_capitalized": is_capitalized, + "is_external": ue_cap["is_external"] if is_capitalized else ue.is_external, + "coef_ue": coef_ue, + "cur_moy_ue": cur_moy_ue, + "moy": moy_ue, + "event_date": ue_cap["event_date"] if is_capitalized else None, + "ue": ue.to_dict(), + "formsemestre_id": ue_cap["formsemestre_id"] if is_capitalized else None, + "capitalized_ue_id": ue_cap["ue_id"] if is_capitalized else None, + } + + def get_etud_ue_cap_coef(self, etudid, ue, ue_cap): + """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' } + """ + # 1- Coefficient explicitement déclaré dans le semestre courant pour cette UE ? + ue_coef_db = FormSemestreUECoef.query.filter_by( + formsemestre_id=self.formsemestre.id, ue_id=ue.id + ).first() + if ue_coef_db is not None: + return ue_coef_db.coefficient + + # En APC: somme des coefs des modules vers cette UE + # En classique: Capitalisation UE externe: quel coef appliquer ? + # En ScoDoc 7, calculait la somme des coefs dans l'UE du semestre d'origine + # ici si l'étudiant est inscrit dans le semestre courant, + # somme des coefs des modules de l'UE auxquels il est inscrit + return self.compute_etud_ue_coef(etudid, ue) + # Pour raccorder le code des anciens codes qui attendent une NoteTable class NotesTableCompat(ResultatsSemestre): @@ -189,7 +304,6 @@ class NotesTableCompat(ResultatsSemestre): self.moy_moy = "NA" self.expr_diagnostics = "" self.parcours = self.formsemestre.formation.get_parcours() - self.validations = None def get_etudids(self, sorted=False) -> list[int]: """Liste des etudids inscrits, incluant les démissionnaires. @@ -353,16 +467,6 @@ class NotesTableCompat(ResultatsSemestre): "ects_fond": 0.0, # not implemented (anciennemment pour école ingé) } - def get_etud_ue_status(self, etudid: int, ue_id: int): - coef_ue = self.etud_coef_ue_df[ue_id][etudid] - return { - "is_capitalized": False, # XXX TODO - "is_external": False, # XXX TODO - "coef_ue": coef_ue, # XXX TODO - "cur_moy_ue": self.etud_moy_ue[ue_id][etudid], - "moy": self.etud_moy_ue[ue_id][etudid], - } - def get_etud_rang(self, etudid: int): return self.etud_moy_gen_ranks.get(etudid, 99999) # XXX diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index e4b8fc8c8..932e3fecb 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -427,10 +427,12 @@ class FormSemestreUECoef(db.Model): formsemestre_id = db.Column( db.Integer, db.ForeignKey("notes_formsemestre.id"), + index=True, ) ue_id = db.Column( db.Integer, db.ForeignKey("notes_ue.id"), + index=True, ) coefficient = db.Column(db.Float, nullable=False) diff --git a/app/scodoc/notes_table.py b/app/scodoc/notes_table.py index b9b4630ce..e5077543b 100644 --- a/app/scodoc/notes_table.py +++ b/app/scodoc/notes_table.py @@ -703,11 +703,12 @@ class NotesTable: 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 + '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, diff --git a/app/scodoc/sco_pvjury.py b/app/scodoc/sco_pvjury.py index ecf2d4887..4479409d0 100644 --- a/app/scodoc/sco_pvjury.py +++ b/app/scodoc/sco_pvjury.py @@ -165,10 +165,7 @@ def _comp_ects_capitalises_by_ue_code(nt, etudid): for ue in ues: ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"]) if ue_status["is_capitalized"]: - try: - ects_val = float(ue_status["ue"]["ects"]) - except (ValueError, TypeError): - ects_val = 0.0 + ects_val = float(ue_status["ue"]["ects"] or 0.0) ects_by_ue_code[ue["ue_code"]] = ects_val return ects_by_ue_code