diff --git a/app/comp/res_compat.py b/app/comp/res_compat.py index 0e6cf757..7d23bea4 100644 --- a/app/comp/res_compat.py +++ b/app/comp/res_compat.py @@ -408,7 +408,7 @@ class NotesTableCompat(ResultatsSemestre): de ce module. Évaluation "complete" ssi toutes notes saisies ou en attente. """ - modimpl = db.session.get(ModuleImpl, moduleimpl_id) + modimpl: ModuleImpl = db.session.get(ModuleImpl, moduleimpl_id) modimpl_results = self.modimpls_results.get(moduleimpl_id) if not modimpl_results: return [] # safeguard diff --git a/app/models/evaluations.py b/app/models/evaluations.py index 63fac264..127c9715 100644 --- a/app/models/evaluations.py +++ b/app/models/evaluations.py @@ -12,9 +12,7 @@ import sqlalchemy as sa from app import db, log from app.models.etudiants import Identite from app.models.events import ScolarNews -from app.models.moduleimpls import ModuleImpl from app.models.notes import NotesNotes -from app.models.ues import UniteEns from app.scodoc import sco_cache from app.scodoc.sco_exceptions import AccessDenied, ScoValueError @@ -67,7 +65,7 @@ class Evaluation(db.Model): @classmethod def create( cls, - moduleimpl: ModuleImpl = None, + moduleimpl: "ModuleImpl" = None, date_debut: datetime.datetime = None, date_fin: datetime.datetime = None, description=None, @@ -114,7 +112,7 @@ class Evaluation(db.Model): @classmethod def get_new_numero( - cls, moduleimpl: ModuleImpl, date_debut: datetime.datetime + cls, moduleimpl: "ModuleImpl", date_debut: datetime.datetime ) -> int: """Get a new numero for an evaluation in this moduleimpl If necessary, renumber existing evals to make room for a new one. @@ -145,7 +143,7 @@ class Evaluation(db.Model): "delete evaluation (commit) (check permission)" from app.scodoc import sco_evaluation_db - modimpl: ModuleImpl = self.moduleimpl + modimpl: "ModuleImpl" = self.moduleimpl if not modimpl.can_edit_evaluation(current_user): raise AccessDenied( f"Modification évaluation impossible pour {current_user.get_nomplogin()}" @@ -239,7 +237,7 @@ class Evaluation(db.Model): check_convert_evaluation_args(self.moduleimpl, data) if data.get("numero") is None: data["numero"] = Evaluation.get_max_numero(self.moduleimpl.id) + 1 - for k in self.__dict__.keys(): + for k in self.__dict__: if k != "_sa_instance_state" and k != "id" and k in data: setattr(self, k, data[k]) @@ -257,7 +255,7 @@ class Evaluation(db.Model): @classmethod def moduleimpl_evaluation_renumber( - cls, moduleimpl: ModuleImpl, only_if_unumbered=False + cls, moduleimpl: "ModuleImpl", only_if_unumbered=False ): """Renumber evaluations in this moduleimpl, according to their date. (numero=0: oldest one) Needed because previous versions of ScoDoc did not have eval numeros @@ -394,6 +392,8 @@ class Evaluation(db.Model): """set poids vers les UE (remplace existants) ue_poids_dict = { ue_id : poids } """ + from app.models.ues import UniteEns + L = [] for ue_id, poids in ue_poids_dict.items(): ue = db.session.get(UniteEns, ue_id) @@ -474,7 +474,7 @@ class EvaluationUEPoids(db.Model): backref=db.backref("ue_poids", cascade="all, delete-orphan"), ) ue = db.relationship( - UniteEns, + "UniteEns", backref=db.backref("evaluation_ue_poids", cascade="all, delete-orphan"), ) @@ -506,7 +506,7 @@ def evaluation_enrich_dict(e: Evaluation, e_dict: dict): return e_dict -def check_convert_evaluation_args(moduleimpl: ModuleImpl, data: dict): +def check_convert_evaluation_args(moduleimpl: "ModuleImpl", data: dict): """Check coefficient, dates and duration, raises exception if invalid. Convert date and time strings to date and time objects. @@ -606,19 +606,6 @@ def heure_to_time(heure: str) -> datetime.time: return datetime.time(int(h), int(m)) -def _time_duration_HhM(heure_debut: str, heure_fin: str) -> int: - """duree (nb entier de minutes) entre deux heures a notre format - ie 12h23 - """ - if heure_debut and heure_fin: - h0, m0 = [int(x) for x in heure_debut.split("h")] - h1, m1 = [int(x) for x in heure_fin.split("h")] - d = (h1 - h0) * 60 + (m1 - m0) - return d - else: - return None - - def _moduleimpl_evaluation_insert_before( evaluations: list[Evaluation], next_eval: Evaluation ) -> int: diff --git a/app/models/events.py b/app/models/events.py index 06dbe558..fc9fbde0 100644 --- a/app/models/events.py +++ b/app/models/events.py @@ -13,7 +13,6 @@ from app import email from app import log from app.auth.models import User from app.models import SHORT_STR_LEN -from app.models.moduleimpls import ModuleImpl import app.scodoc.sco_utils as scu from app.scodoc import sco_preferences @@ -181,6 +180,7 @@ class ScolarNews(db.Model): None si inexistant """ from app.models.formsemestre import FormSemestre + from app.models.moduleimpls import ModuleImpl formsemestre_id = None if self.type == self.NEWS_INSCR: diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 10875cbf..8ddf542c 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -390,7 +390,7 @@ class FormSemestre(db.Model): Module.numero, Module.code, Evaluation.numero, - Evaluation.date_debut.desc(), + Evaluation.date_debut, ) .all() ) diff --git a/app/models/moduleimpls.py b/app/models/moduleimpls.py index 77eba7da..7b9c3596 100644 --- a/app/models/moduleimpls.py +++ b/app/models/moduleimpls.py @@ -3,12 +3,14 @@ """ import pandas as pd from flask_sqlalchemy.query import Query +import sqlalchemy as sa from app import db from app.auth.models import User from app.comp import df_cache from app.models import APO_CODE_STR_LEN from app.models.etudiants import Identite +from app.models.evaluations import Evaluation from app.models.modules import Module from app.scodoc.sco_exceptions import AccessDenied, ScoLockedSemError from app.scodoc.sco_permissions import Permission @@ -38,7 +40,13 @@ class ModuleImpl(db.Model): # formule de calcul moyenne: computation_expr = db.Column(db.Text()) - evaluations = db.relationship("Evaluation", lazy="dynamic", backref="moduleimpl") + evaluations = db.relationship( + "Evaluation", + lazy="dynamic", + backref="moduleimpl", + order_by=(Evaluation.numero, Evaluation.date_debut), + ) + "évaluations, triées par numéro et dates croissants, donc la plus ancienne d'abord." enseignants = db.relationship( "User", secondary="notes_modules_enseignants", diff --git a/app/scodoc/sco_evaluation_check_abs.py b/app/scodoc/sco_evaluation_check_abs.py index 41299056..f10c3111 100644 --- a/app/scodoc/sco_evaluation_check_abs.py +++ b/app/scodoc/sco_evaluation_check_abs.py @@ -28,6 +28,7 @@ """Vérification des absences à une évaluation """ from flask import url_for, g +from flask_sqlalchemy.query import Query from app import db from app.models import Evaluation, FormSemestre, Identite, Assiduite @@ -37,9 +38,6 @@ from app.scodoc import sco_evaluations from app.scodoc import sco_evaluation_db from app.scodoc import sco_groups -from flask_sqlalchemy.query import Query -from sqlalchemy import or_, and_ - def evaluation_check_absences(evaluation: Evaluation): """Vérifie les absences au moment de cette évaluation. @@ -78,11 +76,11 @@ def evaluation_check_absences(evaluation: Evaluation): # Les notes: notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation.id) - ValButAbs = [] # une note mais noté absent - AbsNonSignalee = [] # note ABS mais pas noté absent - ExcNonSignalee = [] # note EXC mais pas noté absent - ExcNonJust = [] # note EXC mais absent non justifie - AbsButExc = [] # note ABS mais justifié + note_but_abs = [] # une note mais noté absent + abs_non_signalee = [] # note ABS mais pas noté absent + exc_non_signalee = [] # note EXC mais pas noté absent + exc_non_just = [] # note EXC mais absent non justifie + abs_but_exc = [] # note ABS mais justifié for etudid in etudids: if etudid in notes_db: val = notes_db[etudid]["value"] @@ -92,50 +90,43 @@ def evaluation_check_absences(evaluation: Evaluation): and val != scu.NOTES_ATTENTE ) and etudid in abs_etudids: # note valide et absent - ValButAbs.append(etudid) + note_but_abs.append(etudid) if val is None and not etudid in abs_etudids: # absent mais pas signale comme tel - AbsNonSignalee.append(etudid) + abs_non_signalee.append(etudid) if val == scu.NOTES_NEUTRALISE and not etudid in abs_etudids: # Neutralisé mais pas signale absent - ExcNonSignalee.append(etudid) + exc_non_signalee.append(etudid) if val == scu.NOTES_NEUTRALISE and etudid in abs_nj_etudids: # EXC mais pas justifié - ExcNonJust.append(etudid) + exc_non_just.append(etudid) if val is None and etudid in just_etudids: # ABS mais justificatif - AbsButExc.append(etudid) + abs_but_exc.append(etudid) - return ValButAbs, AbsNonSignalee, ExcNonSignalee, ExcNonJust, AbsButExc + return note_but_abs, abs_non_signalee, exc_non_signalee, exc_non_just, abs_but_exc def evaluation_check_absences_html( evaluation: Evaluation, with_header=True, show_ok=True ): """Affiche état vérification absences d'une évaluation""" - am, pm = evaluation.is_matin(), evaluation.is_apresmidi() - # 1 si matin, 0 si apres midi, 2 si toute la journee: - match am, pm: - case False, True: - demijournee = 0 - case True, False: - demijournee = 1 - case _: - demijournee = 2 - ( - ValButAbs, - AbsNonSignalee, - ExcNonSignalee, - ExcNonJust, - AbsButExc, + note_but_abs, # une note alors qu'il était signalé abs + abs_non_signalee, # note ABS alors que pas signalé abs + exc_non_signalee, # note EXC alors que pas signalé abs + exc_non_just, # note EXC alors que pas de justif + abs_but_exc, # note ABS alors qu'il y a un justif ) = evaluation_check_absences(evaluation) if with_header: H = [ - html_sco_header.html_sem_header("Vérification absences à l'évaluation"), + html_sco_header.html_sem_header( + "Vérification absences à l'évaluation", + formsemestre_id=evaluation.moduleimpl.formsemestre_id, + ), sco_evaluations.evaluation_describe(evaluation_id=evaluation.id), - """
Vérification de la cohérence entre les notes saisies + """
Vérification de la cohérence entre les notes saisies et les absences signalées.
""", ] else: @@ -148,10 +139,10 @@ def evaluation_check_absences_html( } """ ] if ( - not ValButAbs - and not AbsNonSignalee - and not ExcNonSignalee - and not ExcNonJust + not note_but_abs + and not abs_non_signalee + and not exc_non_signalee + and not exc_non_just ): H.append(': ok') H.append("") @@ -171,46 +162,50 @@ def evaluation_check_absences_html( ) if linkabs: url = url_for( - "assiduites.signal_evaluation_abs", + "assiduites.signale_evaluation_abs", etudid=etudid, evaluation_id=evaluation.id, scodoc_dept=g.scodoc_dept, ) H.append( - f"""signaler cette absence""" + f"""signaler cette absence""" ) H.append("") H.append("") - if ValButAbs or show_ok: + if note_but_abs or show_ok: H.append( - "Vérification de la cohérence entre les notes saisies et les absences signalées. + """
Vérification de la cohérence entre les notes saisies
+ et les absences signalées.
Sont listés tous les modules avec des évaluations.
Aucune action n'est effectuée:
il vous appartient de corriger les erreurs détectées si vous le jugez nécessaire.
sans date ") diff --git a/app/scodoc/sco_liste_notes.py b/app/scodoc/sco_liste_notes.py index 2f065993..467808b3 100644 --- a/app/scodoc/sco_liste_notes.py +++ b/app/scodoc/sco_liste_notes.py @@ -33,18 +33,15 @@ import numpy as np import flask from flask import url_for, g, request -from app import db, log -from app import models from app.comp import res_sem from app.comp import moy_mod from app.comp.moy_mod import ModuleImplResults -from app.comp.res_compat import NotesTableCompat from app.comp.res_but import ResultatsSemestreBUT +from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre, Module from app.models.etudiants import Identite from app.models.evaluations import Evaluation from app.models.moduleimpls import ModuleImpl -import app.scodoc.notesdb as ndb from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.sco_etud import etud_sort_key @@ -54,58 +51,58 @@ from app.scodoc import sco_groups from app.scodoc import sco_preferences from app.scodoc import sco_users import app.scodoc.sco_utils as scu -import sco_version from app.scodoc.gen_tables import GenTable from app.scodoc.htmlutils import histogram_notes +import sco_version def do_evaluation_listenotes( evaluation_id=None, moduleimpl_id=None, fmt="html" -) -> tuple[str, str]: +) -> tuple[str | flask.Response, str]: """ Affichage des notes d'une évaluation (si evaluation_id) ou de toutes les évaluations d'un module (si moduleimpl_id) """ mode = None - if moduleimpl_id: + evaluations: list[Evaluation] = [] + if moduleimpl_id is not None: mode = "module" - evals = sco_evaluation_db.get_evaluations_dict({"moduleimpl_id": moduleimpl_id}) - elif evaluation_id: + modimpl = ModuleImpl.query.get_or_404(moduleimpl_id) + evaluations = modimpl.evaluations.all() + elif evaluation_id is not None: mode = "eval" - evals = sco_evaluation_db.get_evaluations_dict({"evaluation_id": evaluation_id}) + evaluations = Evaluation.query.filter_by(id=evaluation_id).all() else: raise ValueError("missing argument: evaluation or module") - if not evals: + if not evaluations: return "
Aucune évaluation !
", "ScoDoc" + evaluation = evaluations[0] + modimpl = evaluation.moduleimpl # il y a au moins une evaluation - eval_dict = evals[0] # il y a au moins une evaluation - modimpl = db.session.get(ModuleImpl, eval_dict["moduleimpl_id"]) # description de l'evaluation - if mode == "eval": + if evaluation_id is not None: H = [sco_evaluations.evaluation_describe(evaluation_id=evaluation_id)] - page_title = f"Notes {eval_dict['description'] or modimpl.module.code}" + page_title = f"Notes {evaluation.description or modimpl.module.code}" else: H = [] page_title = f"Notes {modimpl.module.code}" # groupes - groups = sco_groups.do_evaluation_listegroupes( - eval_dict["evaluation_id"], include_default=True - ) + groups = sco_groups.do_evaluation_listegroupes(evaluation.id, include_default=True) grlabs = [g["group_name"] or "tous" for g in groups] # legendes des boutons grnams = [str(g["group_id"]) for g in groups] # noms des checkbox - if len(evals) > 1: + if len(evaluations) > 1: descr = [ ( "moduleimpl_id", - {"default": eval_dict["moduleimpl_id"], "input_type": "hidden"}, + {"default": modimpl.id, "input_type": "hidden"}, ) ] else: descr = [ ( "evaluation_id", - {"default": eval_dict["evaluation_id"], "input_type": "hidden"}, + {"default": evaluation.id, "input_type": "hidden"}, ) ] if len(grnams) > 1: @@ -148,7 +145,8 @@ def do_evaluation_listenotes( "allowed_values": ("yes",), "labels": ('listing "anonyme"',), "attributes": ('onclick="document.tf.submit();"',), - "template": 'Aucune évaluation !
" - E = evals[0] - moduleimpl_id = E["moduleimpl_id"] - modimpl = ModuleImpl.query.get_or_404(moduleimpl_id) + evaluation = evaluations[0] + modimpl = evaluation.moduleimpl module: Module = modimpl.module formsemestre: FormSemestre = modimpl.formsemestre - is_apc = module.formation.get_cursus().APC_SAE + is_apc = module.formation.is_apc() if is_apc: res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre) is_conforme = modimpl.check_apc_conformity(res) - evals_poids, ues = moy_mod.load_evaluations_poids(moduleimpl_id) + evals_poids, ues = moy_mod.load_evaluations_poids(modimpl.id) if not ues: is_apc = False else: evals_poids, ues = None, None is_conforme = True # (debug) check that all evals are in same module: - for e in evals: - if e["moduleimpl_id"] != moduleimpl_id: + for e in evaluations: + if e.moduleimpl_id != modimpl.id: raise ValueError("invalid evaluations list") if fmt == "xls": @@ -302,11 +299,14 @@ def _make_table_notes( } rows = [] - class KeyManager(dict): # comment : key (pour regrouper les comments a la fin) + class KeyManager(dict): + "comment : key (pour regrouper les comments a la fin)" + def __init__(self): self.lastkey = 1 - def nextkey(self): + def nextkey(self) -> str: + "get new key (int)" r = self.lastkey self.lastkey += 1 # self.lastkey = chr(ord(self.lastkey)+1) @@ -323,7 +323,7 @@ def _make_table_notes( anonymous_lst_key = "etudid" etudid_etats = sco_groups.do_evaluation_listeetuds_groups( - E["evaluation_id"], groups, include_demdef=True + evaluation.id, groups, include_demdef=True ) for etudid, etat in etudid_etats: css_row_class = None @@ -360,7 +360,8 @@ def _make_table_notes( formsemestre_id=formsemestre.id, etudid=etudid, ), - "_nomprenom_td_attrs": f"""id="{etudid}" class="etudinfo" data-sort="{etud.sort_key}" """, + "_nomprenom_td_attrs": f"""id="{etudid}" class="etudinfo" data-sort="{ + etud.sort_key}" """, "prenom": etud.prenom.lower().capitalize(), "nom_usuel": etud.nom_usuel, "nomprenom": etud.nomprenom, @@ -408,10 +409,12 @@ def _make_table_notes( "comment": "", } # Ajoute les notes de chaque évaluation: - for e in evals: - e["eval_state"] = sco_evaluations.do_evaluation_etat(e["evaluation_id"]) + evals_state: dict[int, dict] = {} + for e in evaluations: + evals_state[e.id] = sco_evaluations.do_evaluation_etat(e.id) notes, nb_abs, nb_att = _add_eval_columns( e, + evals_state[e.id], evals_poids, ues, rows, @@ -426,7 +429,7 @@ def _make_table_notes( keep_numeric, fmt=fmt, ) - columns_ids.append(e["evaluation_id"]) + columns_ids.append(e.id) # if anonymous_listing: rows.sort(key=lambda x: x["code"] or "") @@ -436,12 +439,12 @@ def _make_table_notes( # Si module, ajoute la (les) "moyenne(s) du module: if mode == "module": - if len(evals) > 1: + if len(evaluations) > 1: # Moyenne de l'étudiant dans le module # Affichée même en APC à titre indicatif _add_moymod_column( formsemestre.id, - moduleimpl_id, + modimpl.id, rows, columns_ids, titles, @@ -473,7 +476,7 @@ def _make_table_notes( if with_emails: columns_ids += ["email", "emailperso"] # Ajoute lignes en tête et moyennes - if len(evals) > 0 and fmt != "bordereau": + if len(evaluations) > 0 and fmt != "bordereau": rows_head = [row_coefs] if is_apc: rows_head.append(row_poids) @@ -481,22 +484,22 @@ def _make_table_notes( rows = rows_head + rows rows.append(row_moys) # ajout liens HTMl vers affichage une evaluation: - if fmt == "html" and len(evals) > 1: + if fmt == "html" and len(evaluations) > 1: rlinks = {"_table_part": "head"} - for e in evals: - rlinks[e["evaluation_id"]] = "afficher" + for e in evaluations: + rlinks[e.id] = "afficher" rlinks[ - "_" + str(e["evaluation_id"]) + "_help" + "_" + str(e.id) + "_help" ] = "afficher seulement les notes de cette évaluation" - rlinks["_" + str(e["evaluation_id"]) + "_target"] = url_for( + rlinks["_" + str(e.id) + "_target"] = url_for( "notes.evaluation_listenotes", scodoc_dept=g.scodoc_dept, - evaluation_id=e["evaluation_id"], + evaluation_id=e.id, ) - rlinks["_" + str(e["evaluation_id"]) + "_td_attrs"] = ' class="tdlink" ' + rlinks["_" + str(e.id) + "_td_attrs"] = ' class="tdlink" ' rows.append(rlinks) - if len(evals) == 1: # colonne "Rem." seulement si une eval + if len(evaluations) == 1: # colonne "Rem." seulement si une eval if fmt == "html": # pas d'indication d'origine en pdf (pour affichage) columns_ids.append("expl_key") elif fmt == "xls" or fmt == "xml": @@ -514,68 +517,84 @@ def _make_table_notes( gl = "&hide_groups%3Alist=yes" + gl if with_emails: gl = "&with_emails%3Alist=yes" + gl - if len(evals) == 1: - evalname = "%s-%s" % (module.code, ndb.DateDMYtoISO(E["jour"])) - hh = "%s, %s (%d étudiants)" % (E["description"], gr_title, len(etudid_etats)) - filename = scu.make_filename("notes_%s_%s" % (evalname, gr_title_filename)) + if len(evaluations) == 1: + evalname = f"""{module.code}-{ + evaluation.date_debut.replace(tzinfo=None).isoformat() + if evaluation.date_debut else ""}""" + hh = "%s, %s (%d étudiants)" % ( + evaluation.description, + gr_title, + len(etudid_etats), + ) + filename = scu.make_filename(f"notes_{evalname}_{gr_title_filename}") if fmt == "bordereau": - hh = " %d étudiants" % (len(etudid_etats)) - hh += " %d absent" % (nb_abs) - if nb_abs > 1: - hh += "s" - hh += ", %d en attente." % (nb_att) - + hh = f""" {len(etudid_etats)} étudiants { + nb_abs} absent{'s' if nb_abs > 1 else ''}, {nb_att} en attente.""" # Attention: ReportLab supporte seulement 'Répartition des notes:" - + histo - + " | \n",
- '',
- ]
- commentkeys = list(key_mgr.items()) # [ (comment, key), ... ]
- commentkeys.sort(key=lambda x: int(x[1]))
- for comment, key in commentkeys:
- C.append(
- '(%s) %s
|