diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py index ad7c45940..ec9c450a9 100644 --- a/app/comp/moy_mod.py +++ b/app/comp/moy_mod.py @@ -56,6 +56,7 @@ class EvaluationEtat: evaluation_id: int nb_attente: int + nb_notes: int # nb notes d'étudiants inscrits au semestre et au modimpl is_complete: bool def to_dict(self): @@ -168,13 +169,15 @@ class ModuleImplResults: # NULL en base => ABS (= -999) eval_df.fillna(scu.NOTES_ABSENCE, inplace=True) # Ce merge ne garde que les étudiants inscrits au module - # et met à NULL les notes non présentes + # et met à NULL (NaN) les notes non présentes # (notes non saisies ou etuds non inscrits au module): evals_notes = evals_notes.merge( eval_df, how="left", left_index=True, right_index=True ) # Notes en attente: (ne prend en compte que les inscrits, non démissionnaires) eval_notes_inscr = evals_notes[str(evaluation.id)][list(inscrits_module)] + # Nombre de notes (non vides, incluant ATT etc) des inscrits: + nb_notes = eval_notes_inscr.notna().sum() eval_etudids_attente = set( eval_notes_inscr.iloc[ (eval_notes_inscr == scu.NOTES_ATTENTE).to_numpy() @@ -184,6 +187,7 @@ class ModuleImplResults: self.evaluations_etat[evaluation.id] = EvaluationEtat( evaluation_id=evaluation.id, nb_attente=len(eval_etudids_attente), + nb_notes=nb_notes, is_complete=is_complete, ) # au moins une note en ATT dans ce modimpl: diff --git a/app/comp/res_common.py b/app/comp/res_common.py index c6985ff56..9553381b3 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -9,12 +9,13 @@ from collections import Counter, defaultdict from collections.abc import Generator +import datetime from functools import cached_property from operator import attrgetter import numpy as np import pandas as pd - +import sqlalchemy as sa from flask import g, url_for from app import db @@ -22,14 +23,19 @@ from app.comp import res_sem from app.comp.res_cache import ResultatsCache from app.comp.jury import ValidationsSemestre from app.comp.moy_mod import ModuleImplResults -from app.models import FormSemestre, FormSemestreUECoef -from app.models import Identite -from app.models import ModuleImpl, ModuleImplInscription -from app.models import ScolarAutorisationInscription -from app.models.ues import UniteEns +from app.models import ( + Evaluation, + FormSemestre, + FormSemestreUECoef, + Identite, + ModuleImpl, + ModuleImplInscription, + ScolarAutorisationInscription, + UniteEns, +) from app.scodoc.sco_cache import ResultatsSemestreCache from app.scodoc.codes_cursus import UE_SPORT -from app.scodoc.sco_exceptions import ScoValueError +from app.scodoc.sco_exceptions import ScoValueError, ScoTemporaryError from app.scodoc import sco_utils as scu @@ -192,16 +198,80 @@ class ResultatsSemestre(ResultatsCache): *[mr.etudids_attente for mr in self.modimpls_results.values()] ) - # # Etat des évaluations - # # (se substitue à do_evaluation_etat, sans les moyennes par groupes) - # def get_evaluations_etats(evaluation_id: int) -> dict: - # """Renvoie dict avec les clés: - # last_modif - # nb_evals_completes - # nb_evals_en_cours - # nb_evals_vides - # attente - # """ + # Etat des évaluations + def get_evaluation_etat(self, evaluation: Evaluation) -> dict: + """État d'une évaluation + { + "coefficient" : float, # 0 si None + "description" : str, # de l'évaluation, "" si None + "etat" { + "evalcomplete" : bool, + "last_modif" : datetime.datetime | None, # saisie de note la plus récente + "nb_notes" : int, # nb notes d'étudiants inscrits + }, + "jour" : datetime.datetime, # e.date_debut or datetime.datetime(1900, 1, 1) + "publish_incomplete" : bool, + } + """ + mod_results = self.modimpls_results.get(evaluation.moduleimpl_id) + if mod_results is None: + raise ScoTemporaryError() # argh ! + etat = mod_results.evaluations_etat.get(evaluation.id) + if etat is None: + raise ScoTemporaryError() # argh ! + # Date de dernière saisie de note + cursor = db.session.execute( + sa.text( + "SELECT MAX(date) FROM notes_notes WHERE evaluation_id = :evaluation_id" + ), + {"evaluation_id": evaluation.id}, + ) + date_modif = cursor.one_or_none() + last_modif = date_modif[0] if date_modif else None + return { + "coefficient": evaluation.coefficient or 0.0, + "description": evaluation.description or "", + "jour": evaluation.date_debut or datetime.datetime(1900, 1, 1), + "publish_incomplete": evaluation.publish_incomplete, + "etat": { + "evalcomplete": etat.is_complete, + "nb_notes": etat.nb_notes, + "last_modif": last_modif, + }, + } + + def get_mod_evaluation_etat_list(self, modimpl: ModuleImpl) -> list[dict]: + """Liste des états des évaluations de ce module + [ evaluation_etat, ... ] (voir get_evaluation_etat) + trié par (numero desc, date_debut desc) + """ + # nouvelle version 2024-02-02 + return list( + reversed( + [ + self.get_evaluation_etat(evaluation) + for evaluation in modimpl.evaluations + ] + ) + ) + + # modernisation de get_mod_evaluation_etat_list + # utilisé par: + # sco_evaluations.do_evaluation_etat_in_mod + # e["etat"]["evalcomplete"] + # e["etat"]["nb_notes"] + # e["etat"]["last_modif"] + # + # sco_formsemestre_status.formsemestre_description_table + # "jour" (qui est e.date_debut or datetime.date(1900, 1, 1)) + # "description" + # "coefficient" + # e["etat"]["evalcomplete"] + # publish_incomplete + # + # sco_formsemestre_status.formsemestre_tableau_modules + # e["etat"]["nb_notes"] + # # --- JURY... def get_formsemestre_validations(self) -> ValidationsSemestre: diff --git a/app/comp/res_compat.py b/app/comp/res_compat.py index a9b605aa4..0e84538fb 100644 --- a/app/comp/res_compat.py +++ b/app/comp/res_compat.py @@ -423,30 +423,37 @@ class NotesTableCompat(ResultatsSemestre): ) return evaluations - def get_evaluations_etats(self) -> list[dict]: - """Liste de toutes les évaluations du semestre - [ {...evaluation et son etat...} ]""" - # TODO: à moderniser (voir dans ResultatsSemestre) - # utilisé par - # do_evaluation_etat_in_sem + def get_evaluations_etats(self) -> dict[int, dict]: + """ "état" de chaque évaluation du semestre + { + evaluation_id : { + "evalcomplete" : bool, + "last_modif" : datetime | None + "nb_notes" : int, + }, ... + } + """ + # utilisé par do_evaluation_etat_in_sem + evaluations_etats = {} + for modimpl in self.formsemestre.modimpls_sorted: + for evaluation in modimpl.evaluations: + evaluation_etat = self.get_evaluation_etat(evaluation) + evaluations_etats[evaluation.id] = evaluation_etat["etat"] + return evaluations_etats - from app.scodoc import sco_evaluations - - 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 données que l'on a déjà... - return [ - e - for e in self.get_evaluations_etats() - if e["moduleimpl_id"] == moduleimpl_id - ] + # ancienne version < 2024-02-02 + # def get_mod_evaluation_etat_list(self, moduleimpl_id) -> list[dict]: + # """Liste des états des évaluations de ce module + # ordonnée selon (numero desc, date_debut desc) + # """ + # # à moderniser: lent, recharge des données que l'on a déjà... + # # remplacemé par ResultatsSemestre.get_mod_evaluation_etat_list + # # + # return [ + # e + # for e in self.get_evaluations_etats() + # if e["moduleimpl_id"] == moduleimpl_id + # ] def get_moduleimpls_attente(self): """Liste des modimpls du semestre ayant des notes en attente""" diff --git a/app/scodoc/sco_evaluations.py b/app/scodoc/sco_evaluations.py index 382f883dd..487cf9ea4 100644 --- a/app/scodoc/sco_evaluations.py +++ b/app/scodoc/sco_evaluations.py @@ -40,7 +40,7 @@ from app import db from app.auth.models import User from app.comp import res_sem from app.comp.res_compat import NotesTableCompat -from app.models import Evaluation, FormSemestre +from app.models import Evaluation, FormSemestre, ModuleImpl import app.scodoc.sco_utils as scu from app.scodoc.sco_utils import ModuleType @@ -280,82 +280,14 @@ def do_evaluation_etat( } -def do_evaluation_list_in_sem(formsemestre_id, with_etat=True): - """Liste les évaluations de tous les modules de ce semestre. - Triée par module, numero desc, date_debut desc - Donne pour chaque eval son état (voir do_evaluation_etat) - { evaluation_id,nb_inscrits, nb_notes, nb_abs, nb_neutre, moy, median, last_modif ... } - - Exemple: - [ { - 'coefficient': 1.0, - 'description': 'QCM et cas pratiques', - 'etat': { - 'evalattente': False, - 'evalcomplete': True, - 'evaluation_id': 'GEAEVAL82883', - 'gr_incomplets': [], - 'gr_moyennes': [{ - 'gr_median': '12.00', # sur 20 - 'gr_moy': '11.88', - 'gr_nb_att': 0, - 'gr_nb_notes': 166, - 'group_id': 'GEAG266762', - 'group_name': None - }], - 'groups': {'GEAG266762': {'etudid': 'GEAEID80603', - 'group_id': 'GEAG266762', - 'group_name': None, - 'partition_id': 'GEAP266761'} - }, - 'last_modif': datetime.datetime(2015, 12, 3, 15, 15, 16), - 'median': '12.00', - 'moy': '11.84', - 'nb_abs': 2, - 'nb_att': 0, - 'nb_inscrits': 166, - 'nb_neutre': 0, - 'nb_notes': 168, - 'nb_notes_total': 169 - }, - 'evaluation_id': 'GEAEVAL82883', - 'evaluation_type': 0, - 'heure_debut': datetime.time(8, 0), - 'heure_fin': datetime.time(9, 30), - 'jour': datetime.date(2015, 11, 3), // vide => 1/1/1900 - 'moduleimpl_id': 'GEAMIP80490', - 'note_max': 20.0, - 'numero': 0, - 'publish_incomplete': 0, - 'visibulletin': 1} ] - - """ - req = """SELECT E.id AS evaluation_id, E.* - FROM notes_evaluation E, notes_moduleimpl MI - WHERE MI.formsemestre_id = %(formsemestre_id)s - and MI.id = E.moduleimpl_id - ORDER BY MI.id, numero desc, date_debut desc - """ - cnx = ndb.GetDBConnexion() - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) - cursor.execute(req, {"formsemestre_id": formsemestre_id}) - res = cursor.dictfetchall() - # etat de chaque evaluation: - for r in res: - if with_etat: - r["etat"] = do_evaluation_etat(r["evaluation_id"]) - r["jour"] = r["date_debut"] or datetime.date(1900, 1, 1) - - return res - - -def _eval_etat(evals): - """evals: list of mappings (etats) +def _summarize_evals_etats(evals: list[dict]) -> dict: + """Synthétise les états d'une liste d'évaluations + evals: list of mappings (etats), + utilise e["etat"]["evalcomplete"], e["etat"]["nb_notes"], e["etat"]["last_modif"] -> nb_eval_completes, nb_evals_en_cours, nb_evals_vides, date derniere modif Une eval est "complete" ssi tous les etudiants *inscrits* ont une note. - """ nb_evals_completes, nb_evals_en_cours, nb_evals_vides = 0, 0, 0 dates = [] @@ -370,11 +302,8 @@ def _eval_etat(evals): if last_modif is not None: dates.append(e["etat"]["last_modif"]) - if dates: - dates = scu.sort_dates(dates) - last_modif = dates[-1] # date de derniere modif d'une note dans un module - else: - last_modif = "" + # date de derniere modif d'une note dans un module + last_modif = sorted(dates)[-1] if dates else "" return { "nb_evals_completes": nb_evals_completes, @@ -384,37 +313,42 @@ def _eval_etat(evals): } -def do_evaluation_etat_in_sem(formsemestre_id): - """-> nb_eval_completes, nb_evals_en_cours, nb_evals_vides, - date derniere modif, attente - - XXX utilisé par - - formsemestre_status_head - - gen_formsemestre_recapcomplet_xml - - gen_formsemestre_recapcomplet_json - - "nb_evals_completes" - "nb_evals_en_cours" - "nb_evals_vides" - "date_derniere_note" - "last_modif" - "attente" +def do_evaluation_etat_in_sem(formsemestre: FormSemestre) -> dict: + """-> { nb_eval_completes, nb_evals_en_cours, nb_evals_vides, + date derniere modif, attente } """ - formsemestre = FormSemestre.get_formsemestre(formsemestre_id) + # Note: utilisé par + # - formsemestre_status_head + # nb_evals_completes, nb_evals_en_cours, nb_evals_vides, last_modif + # pour la ligne + # Évaluations: 20 ok, 8 en cours, 5 vides (dernière note saisie le 11/01/2024 à 19h49) + # attente + # + # - gen_formsemestre_recapcomplet_xml + # - gen_formsemestre_recapcomplet_json + # nb_evals_completes, nb_evals_en_cours, nb_evals_vides, last_modif + # + # "nb_evals_completes" + # "nb_evals_en_cours" + # "nb_evals_vides" + # "last_modif" + # "attente" + nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) - evals = nt.get_evaluations_etats() - etat = _eval_etat(evals) + evaluations_etats = nt.get_evaluations_etats() + # raccordement moche... + etat = _summarize_evals_etats([{"etat": v} for v in evaluations_etats.values()]) # Ajoute information sur notes en attente etat["attente"] = len(nt.get_moduleimpls_attente()) > 0 return etat -def do_evaluation_etat_in_mod(nt, moduleimpl_id): +def do_evaluation_etat_in_mod(nt, modimpl: ModuleImpl): """état des évaluations dans ce module""" - evals = nt.get_mod_evaluation_etat_list(moduleimpl_id) - etat = _eval_etat(evals) + evals = nt.get_mod_evaluation_etat_list(modimpl) + etat = _summarize_evals_etats(evals) # Il y a-t-il des notes en attente dans ce module ? - etat["attente"] = nt.modimpls_results[moduleimpl_id].en_attente + etat["attente"] = nt.modimpls_results[modimpl.id].en_attente return etat diff --git a/app/scodoc/sco_exceptions.py b/app/scodoc/sco_exceptions.py index 776dd98d6..82339db95 100644 --- a/app/scodoc/sco_exceptions.py +++ b/app/scodoc/sco_exceptions.py @@ -230,3 +230,15 @@ class APIInvalidParams(Exception): class ScoFormationConflict(Exception): """Conflit cohérence formation (APC)""" + + +class ScoTemporaryError(ScoValueError): + """Erreurs temporaires rarissimes (caches ?)""" + + def __init__(self, msg: str = ""): + msg = """ +
"Erreur temporaire
+Veuillez ré-essayer. Si le problème persiste, merci de contacter l'assistance ScoDoc +
+ """ + super().__init__(msg) diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 21b8adf4e..5e42ac75f 100755 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -627,9 +627,7 @@ def formsemestre_description_table( # car l'UE de rattachement n'a pas d'intérêt en BUT rows.append(ue_info) - mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list( - moduleimpl_id=modimpl.id - ) + mod_nb_inscrits = nt.modimpls_results[modimpl.id].nb_inscrits_module enseignants = ", ".join(ens.get_prenomnom() for ens in modimpl.enseignants) row = { @@ -638,7 +636,7 @@ def formsemestre_description_table( "Code": modimpl.module.code or "", "Module": modimpl.module.abbrev or modimpl.module.titre, "_Module_class": "scotext", - "Inscrits": len(mod_inscrits), + "Inscrits": mod_nb_inscrits, "Responsable": sco_users.user_info(modimpl.responsable_id)["nomprenom"], "_Responsable_class": "scotext", "Enseignants": enseignants, @@ -680,7 +678,7 @@ def formsemestre_description_table( if with_evals: # Ajoute lignes pour evaluations - evals = nt.get_mod_evaluation_etat_list(modimpl.id) + evals = nt.get_mod_evaluation_etat_list(modimpl) evals.reverse() # ordre chronologique # Ajoute etat: eval_rows = [] @@ -942,10 +940,10 @@ def html_expr_diagnostic(diagnostics): def formsemestre_status_head(formsemestre_id: int = None, page_title: str = None): """En-tête HTML des pages "semestre" """ - sem: FormSemestre = db.session.get(FormSemestre, formsemestre_id) - if not sem: + formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id) + if not formsemestre: raise ScoValueError("Semestre inexistant (il a peut être été supprimé ?)") - formation: Formation = sem.formation + formation: Formation = formsemestre.formation parcours = formation.get_cursus() page_title = page_title or "Modules de " @@ -957,25 +955,25 @@ def formsemestre_status_head(formsemestre_id: int = None, page_title: str = None f"""Formation: | {formation.titre} """, ] - if sem.semestre_id >= 0: - H.append(f", {parcours.SESSION_NAME} {sem.semestre_id}") - if sem.modalite: - H.append(f" en {sem.modalite}") - if sem.etapes: + if formsemestre.semestre_id >= 0: + H.append(f", {parcours.SESSION_NAME} {formsemestre.semestre_id}") + if formsemestre.modalite: + H.append(f" en {formsemestre.modalite}") + if formsemestre.etapes: H.append( f""" (étape { - sem.etapes_apo_str() or "-" + formsemestre.etapes_apo_str() or "-" })""" ) H.append(" |
Parcours: | @@ -984,7 +982,7 @@ def formsemestre_status_head(formsemestre_id: int = None, page_title: str = None """ ) - evals = sco_evaluations.do_evaluation_etat_in_sem(formsemestre_id) + evals = sco_evaluations.do_evaluation_etat_in_sem(formsemestre) H.append( '|
Évaluations: | %(nb_evals_completes)s ok, %(nb_evals_en_cours)s en cours, %(nb_evals_vides)s vides'
% evals
@@ -1002,11 +1000,11 @@ def formsemestre_status_head(formsemestre_id: int = None, page_title: str = None
"""Il y a des notes en attente !
Le classement des étudiants n'a qu'une valeur indicative."""
)
- if sem.bul_hide_xml:
+ if formsemestre.bul_hide_xml:
warnings.append("""Bulletins non publiés sur la passerelle.""")
- if sem.block_moyennes:
+ if formsemestre.block_moyennes:
warnings.append("Calcul des moyennes bloqué !")
- if sem.semestre_id >= 0 and not sem.est_sur_une_annee():
+ if formsemestre.semestre_id >= 0 and not formsemestre.est_sur_une_annee():
warnings.append("""Ce semestre couvre plusieurs années scolaires !""")
if warnings:
H += [
@@ -1028,18 +1026,14 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
# S'assure que les groupes de parcours sont à jour:
if int(check_parcours):
formsemestre.setup_parcours_groups()
- modimpls = sco_moduleimpl.moduleimpl_withmodule_list(
- formsemestre_id=formsemestre_id
- )
+ modimpls = formsemestre.modimpls_sorted
nt = res_sem.load_formsemestre_results(formsemestre)
# Construit la liste de tous les enseignants de ce semestre:
mails_enseignants = set(u.email for u in formsemestre.responsables)
for modimpl in modimpls:
- mails_enseignants.add(sco_users.user_info(modimpl["responsable_id"])["email"])
- mails_enseignants |= set(
- [sco_users.user_info(m["ens_id"])["email"] for m in modimpl["ens"]]
- )
+ mails_enseignants.add(sco_users.user_info(modimpl.responsable_id)["email"])
+ mails_enseignants |= {u.email for u in modimpl.enseignants if u.email}
can_edit = formsemestre.can_be_edited_by(current_user)
can_change_all_notes = current_user.has_permission(Permission.EditAllNotes) or (
@@ -1089,13 +1083,13 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
if nt.parcours.APC_SAE:
# BUT: tableau ressources puis SAE
ressources = [
- m for m in modimpls if m["module"]["module_type"] == ModuleType.RESSOURCE
+ m for m in modimpls if m.module.module_type == ModuleType.RESSOURCE
]
- saes = [m for m in modimpls if m["module"]["module_type"] == ModuleType.SAE]
+ saes = [m for m in modimpls if m.module.module_type == ModuleType.SAE]
autres = [
m
for m in modimpls
- if m["module"]["module_type"] not in (ModuleType.RESSOURCE, ModuleType.SAE)
+ if m.module.module_type not in (ModuleType.RESSOURCE, ModuleType.SAE)
]
H += [
f"""
@@ -1136,7 +1130,7 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
modimpls_classic = [
m
for m in modimpls
- if m["module"]["module_type"] not in (ModuleType.RESSOURCE, ModuleType.SAE)
+ if m.module.module_type not in (ModuleType.RESSOURCE, ModuleType.SAE)
]
H += [
" ", @@ -1168,8 +1162,10 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True): adrlist = list(mails_enseignants - {None, ""}) if adrlist: H.append( - ' ' - % (",".join(adrlist), len(adrlist)) + f"""""" ) return "".join(H) + html_sco_header.sco_footer() @@ -1189,7 +1185,7 @@ _TABLEAU_MODULES_FOOT = """ |