diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index 87b17546..b73b871a 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -291,15 +291,19 @@ class BulletinBUT: "date_fin": e.date_fin.isoformat() if e.date_fin else None, "description": e.description, "evaluation_type": e.evaluation_type, - "note": { - "value": fmt_note( - eval_notes[etud.id], - note_max=e.note_max, - ), - "min": fmt_note(notes_ok.min(), note_max=e.note_max), - "max": fmt_note(notes_ok.max(), note_max=e.note_max), - "moy": fmt_note(notes_ok.mean(), note_max=e.note_max), - }, + "note": ( + { + "value": fmt_note( + eval_notes[etud.id], + note_max=e.note_max, + ), + "min": fmt_note(notes_ok.min(), note_max=e.note_max), + "max": fmt_note(notes_ok.max(), note_max=e.note_max), + "moy": fmt_note(notes_ok.mean(), note_max=e.note_max), + } + if not e.is_blocked() + else {} + ), "poids": poids, "url": ( url_for( diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py index a977894d..b1af5c06 100644 --- a/app/comp/moy_mod.py +++ b/app/comp/moy_mod.py @@ -35,7 +35,6 @@ moyenne générale d'une UE. """ import dataclasses from dataclasses import dataclass - import numpy as np import pandas as pd import sqlalchemy as sa @@ -151,16 +150,18 @@ class ModuleImplResults: self.evaluations_completes_dict = {} 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 - # ou évaluation déclarée "à prise en compte immédiate" - # Les évaluations de rattrapage et 2eme session sont toujours complètes + # is_complete ssi + # tous les inscrits (non dem) au module ont une note + # ou évaluation déclarée "à prise en compte immédiate" + # ou rattrapage, 2eme session, bonus + # ET pas bloquée par date (is_blocked) etudids_sans_note = inscrits_module - set(eval_df.index) # sans les dem. is_complete = ( (evaluation.evaluation_type != Evaluation.EVALUATION_NORMALE) or (evaluation.publish_incomplete) or (not etudids_sans_note) - ) + ) and not evaluation.is_blocked() self.evaluations_completes.append(is_complete) self.evaluations_completes_dict[evaluation.id] = is_complete self.evals_etudids_sans_note[evaluation.id] = etudids_sans_note @@ -185,7 +186,7 @@ class ModuleImplResults: ].index ) if evaluation.publish_incomplete: - # et en "imédiat", tous ceux sans note + # et en "immédiat", tous ceux sans note eval_etudids_attente |= etudids_sans_note # Synthèse pour état du module: self.etudids_attente |= eval_etudids_attente @@ -276,7 +277,7 @@ class ModuleImplResults: ) / [e.note_max / 20.0 for e in moduleimpl.evaluations] def get_eval_notes_dict(self, evaluation_id: int) -> dict: - """Notes d'une évaulation, brutes, sous forme d'un dict + """Notes d'une évaluation, brutes, sous forme d'un dict { etudid : valeur } avec les valeurs float, ou "ABS" ou EXC """ diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 954f3523..9ba073a4 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -230,8 +230,8 @@ class ResultatsSemestre(ResultatsCache): 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 "", + "coefficient": evaluation.coefficient, + "description": evaluation.description, "evaluation_id": evaluation.id, "jour": evaluation.date_debut or datetime.datetime(1900, 1, 1), "etat": { diff --git a/app/models/evaluations.py b/app/models/evaluations.py index 37e3ac79..cea6c62e 100644 --- a/app/models/evaluations.py +++ b/app/models/evaluations.py @@ -10,6 +10,7 @@ from flask_login import current_user import sqlalchemy as sa from app import db, log +from app import models from app.models.etudiants import Identite from app.models.events import ScolarNews from app.models.notes import NotesNotes @@ -24,7 +25,7 @@ NOON = datetime.time(12, 00) DEFAULT_EVALUATION_TIME = datetime.time(8, 0) -class Evaluation(db.Model): +class Evaluation(models.ScoDocModel): """Evaluation (contrôle, examen, ...)""" __tablename__ = "notes_evaluation" @@ -36,9 +37,9 @@ class Evaluation(db.Model): ) date_debut = db.Column(db.DateTime(timezone=True), nullable=True) date_fin = db.Column(db.DateTime(timezone=True), nullable=True) - description = db.Column(db.Text) - note_max = db.Column(db.Float) - coefficient = db.Column(db.Float) + description = db.Column(db.Text, nullable=False) + note_max = db.Column(db.Float, nullable=False) + coefficient = db.Column(db.Float, nullable=False) visibulletin = db.Column( db.Boolean, nullable=False, default=True, server_default="true" ) @@ -46,10 +47,14 @@ class Evaluation(db.Model): publish_incomplete = db.Column( db.Boolean, nullable=False, default=False, server_default="false" ) - # type d'evaluation: 0 normale, 1 rattrapage, 2 "2eme session" + "prise en compte immédiate" evaluation_type = db.Column( db.Integer, nullable=False, default=0, server_default="0" ) + "type d'evaluation: 0 normale, 1 rattrapage, 2 2eme session, 3 bonus" + blocked_until = db.Column(db.DateTime(timezone=True), nullable=True) + "date de prise en compte" + BLOCKED_FOREVER = datetime.datetime(2666, 12, 31, tzinfo=scu.TIME_ZONE) # ordre de presentation (par défaut, le plus petit numero # est la plus ancienne eval): numero = db.Column(db.Integer, nullable=False, default=0) @@ -79,6 +84,7 @@ class Evaluation(db.Model): date_fin: datetime.datetime = None, description=None, note_max=None, + blocked_until=None, coefficient=None, visibulletin=None, publish_incomplete=None, @@ -208,6 +214,10 @@ class Evaluation(db.Model): def to_dict_api(self) -> dict: "Représentation dict pour API JSON" return { + "blocked": self.is_blocked(), + "blocked_until": ( + self.blocked_until.isoformat() if self.blocked_until else "" + ), "coefficient": self.coefficient, "date_debut": self.date_debut.isoformat() if self.date_debut else "", "date_fin": self.date_fin.isoformat() if self.date_fin else "", @@ -244,14 +254,14 @@ class Evaluation(db.Model): return e_dict - def from_dict(self, data): - """Set evaluation attributes from given dict values.""" - 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__: - if k != "_sa_instance_state" and k != "id" and k in data: - setattr(self, k, data[k]) + def convert_dict_fields(self, args: dict) -> dict: + """Convert fields in the given dict. No other side effect. + returns: dict to store in model's db. + """ + check_convert_evaluation_args(self.moduleimpl, args) + if args.get("numero") is None: + args["numero"] = Evaluation.get_max_numero(self.moduleimpl.id) + 1 + return args @classmethod def get_evaluation( @@ -370,19 +380,6 @@ class Evaluation(db.Model): Chaine vide si non renseignée.""" return self.date_fin.time().isoformat("minutes") if self.date_fin else "" - def clone(self, not_copying=()): - """Clone, not copying the given attrs - Attention: la copie n'a pas d'id avant le prochain commit - """ - d = dict(self.__dict__) - d.pop("id") # get rid of id - d.pop("_sa_instance_state") # get rid of SQLAlchemy special attr - for k in not_copying: - d.pop(k) - copy = self.__class__(**d) - db.session.add(copy) - return copy - def is_matin(self) -> bool: "Evaluation commençant le matin (faux si pas de date)" if not self.date_debut: @@ -395,6 +392,14 @@ class Evaluation(db.Model): return False return self.date_debut.time() >= NOON + def is_blocked(self, now=None) -> bool: + "True si prise en compte bloquée" + if self.blocked_until is None: + return False + if now is None: + now = datetime.datetime.now(scu.TIME_ZONE) + return self.blocked_until > now + def set_default_poids(self) -> bool: """Initialize les poids vers les UE à leurs valeurs par défaut C'est à dire à 1 si le coef. module/UE est non nul, 0 sinon. @@ -621,6 +626,8 @@ def check_convert_evaluation_args(moduleimpl: "ModuleImpl", data: dict): "Heures de l'évaluation incohérentes !", dest_url="javascript:history.back();", ) + if "blocked_until" in data: + data["blocked_until"] = data["blocked_until"] or None def heure_to_time(heure: str) -> datetime.time: diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 9d2f5b13..09c1d305 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -93,6 +93,10 @@ class FormSemestre(db.Model): db.Boolean(), nullable=False, default=False, server_default="false" ) "Si vrai, la moyenne générale indicative BUT n'est pas calculée" + mode_calcul_moyennes = db.Column( + db.Integer, nullable=False, default=0, server_default="0" + ) + "pour usage futur" gestion_semestrielle = db.Column( db.Boolean(), nullable=False, default=False, server_default="false" ) diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index 69c13df3..1b70d385 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -318,7 +318,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"): if nt.bonus_ues is not None: u["cur_moy_ue_txt"] += " (+ues)" u["moy_ue_txt"] = scu.fmt_note(ue_status["moy"]) - if ue_status["coef_ue"] != None: + if ue_status["coef_ue"] is not None: u["coef_ue_txt"] = scu.fmt_coef(ue_status["coef_ue"]) else: u["coef_ue_txt"] = "-" @@ -558,6 +558,8 @@ def _ue_mod_bulletin( ).order_by(Evaluation.numero, Evaluation.date_debut) # (plus ancienne d'abord) for e in all_evals: + if e.is_blocked(): + continue # ignore évaluations bloquées if not e.visibulletin and version != "long": continue is_complete = e.id in complete_eval_ids @@ -625,7 +627,7 @@ def _ue_mod_bulletin( ) ): # ne liste pas les eval malus sans notes - # ni les rattrapages et sessions 2 si pas de note + # ni les rattrapages, sessions 2 et bonus si pas de note if e.id in complete_eval_ids: mod["evaluations"].append(e_dict) else: diff --git a/app/scodoc/sco_bulletins_json.py b/app/scodoc/sco_bulletins_json.py index 3bce083a..0481e6f9 100644 --- a/app/scodoc/sco_bulletins_json.py +++ b/app/scodoc/sco_bulletins_json.py @@ -25,7 +25,7 @@ # ############################################################################## -"""Génération du bulletin en format JSON +"""Génération du bulletin en format JSON (formations classiques) """ import datetime diff --git a/app/scodoc/sco_evaluation_edit.py b/app/scodoc/sco_evaluation_edit.py index c7b0dd67..11418a99 100644 --- a/app/scodoc/sco_evaluation_edit.py +++ b/app/scodoc/sco_evaluation_edit.py @@ -108,7 +108,7 @@ def evaluation_create_form( raise ValueError("missing evaluation_id parameter") initvalues = evaluation.to_dict() moduleimpl_id = initvalues["moduleimpl_id"] - submitlabel = "Modifier les données" + submitlabel = "Modifier l'évaluation" action = "Modification d'une évaluation" link = "" # Note maximale actuelle dans cette éval ? @@ -142,6 +142,15 @@ def evaluation_create_form( else: poids = 0.0 initvalues[f"poids_{ue.id}"] = poids + # Blocage + if edit: + initvalues["blocked"] = evaluation.is_blocked() + initvalues["blocked_until"] = ( + evaluation.blocked_until.strftime("%d/%m/%Y") + if evaluation.blocked_until + and evaluation.blocked_until < Evaluation.BLOCKED_FOREVER + else "" + ) # form = [ ("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}), @@ -260,6 +269,7 @@ def evaluation_create_form( "explanation": """importance de l'évaluation (multiplie les poids ci-dessous). Non utilisé pour les bonus.""", "allow_null": False, + "dom_id": "evaluation-edit-coef", }, ), ] @@ -301,6 +311,28 @@ def evaluation_create_form( }, ), ) + # Bloquage / date prise en compte + form += [ + ( + "blocked", + { + "input_type": "boolcheckbox", + "title": "Bloquer la prise en compte", + "explanation": """empêche la prise en compte + (ne sera pas visible sur les bulletins ni dans les tableaux)""", + "dom_id": "evaluation-edit-blocked", + }, + ), + ( + "blocked_until", + { + "input_type": "datedmy", + "title": "Date déblocage", + "size": 12, + "explanation": "sera débloquée à partir de cette date", + }, + ), + ] tf = TrivialFormulator( request.base_url, vals, @@ -331,7 +363,9 @@ def evaluation_create_form( + "\n".join(H) + "\n" + tf[1] - + render_template("scodoc/help/evaluations.j2", is_apc=is_apc) + + render_template( + "scodoc/help/evaluations.j2", is_apc=is_apc, modimpl=modimpl + ) + render_template("sco_timepicker.j2") + html_sco_header.sco_footer() ) @@ -357,7 +391,8 @@ def evaluation_create_form( raise ScoValueError("Heure début invalide") from exc args["date_debut"] = datetime.datetime.combine(date_debut, heure_debut) args.pop("heure_debut", None) - # note: ce formulaire ne permet de créer que des évaluation avec debut et fin sur le même jour. + # note: ce formulaire ne permet de créer que des évaluations + # avec debut et fin sur le même jour. if date_debut and args.get("heure_fin"): try: heure_fin = heure_to_time(args["heure_fin"]) @@ -365,6 +400,19 @@ def evaluation_create_form( raise ScoValueError("Heure fin invalide") from exc args["date_fin"] = datetime.datetime.combine(date_debut, heure_fin) args.pop("heure_fin", None) + # Blocage: + if args.get("blocked"): + if args.get("blocked_until"): + try: + args["blocked_until"] = datetime.datetime.strptime( + args["blocked_until"], "%d/%m/%Y" + ) + except ValueError as exc: + raise ScoValueError("Date déblocage (j/m/a) invalide") from exc + else: # bloquage coché sans date + args["blocked_until"] = Evaluation.BLOCKED_FOREVER + else: # si pas coché, efface date déblocage + args["blocked_until"] = None # if edit: evaluation.from_dict(args) diff --git a/app/scodoc/sco_evaluations.py b/app/scodoc/sco_evaluations.py index d157e0a3..8931faf2 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, ModuleImpl +from app.models import Evaluation, FormSemestre, ModuleImpl, Module import app.scodoc.sco_utils as scu from app.scodoc.sco_utils import ModuleType @@ -48,7 +48,6 @@ from app.scodoc.gen_tables import GenTable from app.scodoc import html_sco_header from app.scodoc import sco_cal from app.scodoc import sco_evaluation_db -from app.scodoc import sco_edit_module from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_groups from app.scodoc import sco_moduleimpl @@ -113,6 +112,7 @@ def do_evaluation_etat( nb_neutre, nb_att, moy, median, mini, maxi : # notes, en chaine, sur 20 + maxi_num : note max, numérique last_modif: datetime, * gr_complets, gr_incomplets, evalcomplete * @@ -129,11 +129,12 @@ def do_evaluation_etat( ) # { etudid : note } # ---- Liste des groupes complets et incomplets - E = sco_evaluation_db.get_evaluations_dict(args={"evaluation_id": evaluation_id})[0] - M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] - Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] - is_malus = Mod["module_type"] == ModuleType.MALUS # True si module de malus - formsemestre_id = M["formsemestre_id"] + evaluation = Evaluation.get_evaluation(evaluation_id) + modimpl: ModuleImpl = evaluation.moduleimpl + module: Module = modimpl.module + + is_malus = module.module_type == ModuleType.MALUS # True si module de malus + formsemestre_id = modimpl.formsemestre_id # Si partition_id is None, prend 'all' ou bien la premiere: if partition_id is None: if select_first_partition: @@ -149,9 +150,7 @@ def do_evaluation_etat( insem = sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits( formsemestre_id ) - insmod = sco_moduleimpl.do_moduleimpl_inscription_list( - moduleimpl_id=E["moduleimpl_id"] - ) + insmod = sco_moduleimpl.do_moduleimpl_inscription_list(moduleimpl_id=modimpl.id) insmodset = {x["etudid"] for x in insmod} # retire de insem ceux qui ne sont pas inscrits au module ins = [i for i in insem if i["etudid"] in insmodset] @@ -174,9 +173,9 @@ def do_evaluation_etat( maxi_num = None else: median = scu.fmt_note(median_num) - moy = scu.fmt_note(moy_num, E["note_max"]) - mini = scu.fmt_note(mini_num, E["note_max"]) - maxi = scu.fmt_note(maxi_num, E["note_max"]) + moy = scu.fmt_note(moy_num, evaluation.note_max) + mini = scu.fmt_note(mini_num, evaluation.note_max) + maxi = scu.fmt_note(maxi_num, evaluation.note_max) # cherche date derniere modif note if len(etuds_notes_dict): t = [x["date"] for x in etuds_notes_dict.values()] @@ -218,14 +217,16 @@ def do_evaluation_etat( gr_incomplets = list(group_nb_missing.keys()) gr_incomplets.sort() - complete = (total_nb_missing == 0) or ( - E["evaluation_type"] != Evaluation.EVALUATION_NORMALE + complete = ( + (total_nb_missing == 0) + or (evaluation.evaluation_type != Evaluation.EVALUATION_NORMALE) + and not evaluation.is_blocked() ) evalattente = (total_nb_missing > 0) and ( - (total_nb_missing == total_nb_att) or E["publish_incomplete"] + (total_nb_missing == total_nb_att) or evaluation.publish_incomplete ) # mais ne met pas en attente les evals immediates sans aucune notes: - if E["publish_incomplete"] and nb_notes == 0: + if evaluation.publish_incomplete and nb_notes == 0: evalattente = False # Calcul moyenne dans chaque groupe de TD @@ -236,10 +237,10 @@ def do_evaluation_etat( { "group_id": group_id, "group_name": group_by_id[group_id]["group_name"], - "gr_moy": scu.fmt_note(gr_moy, E["note_max"]), - "gr_median": scu.fmt_note(gr_median, E["note_max"]), - "gr_mini": scu.fmt_note(gr_mini, E["note_max"]), - "gr_maxi": scu.fmt_note(gr_maxi, E["note_max"]), + "gr_moy": scu.fmt_note(gr_moy, evaluation.note_max), + "gr_median": scu.fmt_note(gr_median, evaluation.note_max), + "gr_mini": scu.fmt_note(gr_mini, evaluation.note_max), + "gr_maxi": scu.fmt_note(gr_maxi, evaluation.note_max), "gr_nb_notes": len(notes), "gr_nb_att": len([x for x in notes if x == scu.NOTES_ATTENTE]), } diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py index 0d4095b6..ea72d2b5 100644 --- a/app/scodoc/sco_excel.py +++ b/app/scodoc/sco_excel.py @@ -534,7 +534,7 @@ def excel_feuille_saisie(evaluation: "Evaluation", titreannee, description, line # description evaluation ws.append_single_cell_row(scu.unescape_html(description), style_titres) ws.append_single_cell_row( - f"Evaluation {evaluation.descr_date()} (coef. {(evaluation.coefficient or 0.0):g})", + f"Evaluation {evaluation.descr_date()} (coef. {(evaluation.coefficient):g})", style, ) # ligne blanche diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py index 28021e13..336d49ff 100644 --- a/app/scodoc/sco_moduleimpl_status.py +++ b/app/scodoc/sco_moduleimpl_status.py @@ -531,6 +531,10 @@ def _ligne_evaluation( if not evaluation.visibulletin: tr_class += " non_visible_inter" tr_class_1 = "mievr" + if evaluation.is_blocked(): + tr_class += " evaluation_blocked" + tr_class_1 += " evaluation_blocked" + if not first_eval: H.append("""