Update opolka/ScoDoc from ScoDoc/ScoDoc #2

Merged
opolka merged 1272 commits from ScoDoc/ScoDoc:master into master 2024-05-27 09:11:04 +02:00
17 changed files with 272 additions and 89 deletions
Showing only changes of commit 1c01d987be - Show all commits

View File

@ -291,7 +291,8 @@ class BulletinBUT:
"date_fin": e.date_fin.isoformat() if e.date_fin else None, "date_fin": e.date_fin.isoformat() if e.date_fin else None,
"description": e.description, "description": e.description,
"evaluation_type": e.evaluation_type, "evaluation_type": e.evaluation_type,
"note": { "note": (
{
"value": fmt_note( "value": fmt_note(
eval_notes[etud.id], eval_notes[etud.id],
note_max=e.note_max, note_max=e.note_max,
@ -299,7 +300,10 @@ class BulletinBUT:
"min": fmt_note(notes_ok.min(), 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), "max": fmt_note(notes_ok.max(), note_max=e.note_max),
"moy": fmt_note(notes_ok.mean(), note_max=e.note_max), "moy": fmt_note(notes_ok.mean(), note_max=e.note_max),
}, }
if not e.is_blocked()
else {}
),
"poids": poids, "poids": poids,
"url": ( "url": (
url_for( url_for(

View File

@ -35,7 +35,6 @@ moyenne générale d'une UE.
""" """
import dataclasses import dataclasses
from dataclasses import dataclass from dataclasses import dataclass
import numpy as np import numpy as np
import pandas as pd import pandas as pd
import sqlalchemy as sa import sqlalchemy as sa
@ -151,16 +150,18 @@ class ModuleImplResults:
self.evaluations_completes_dict = {} self.evaluations_completes_dict = {}
for evaluation in moduleimpl.evaluations: for evaluation in moduleimpl.evaluations:
eval_df = self._load_evaluation_notes(evaluation) eval_df = self._load_evaluation_notes(evaluation)
# is_complete ssi tous les inscrits (non dem) au semestre ont une note # is_complete ssi
# tous les inscrits (non dem) au module ont une note
# ou évaluation déclarée "à prise en compte immédiate" # ou évaluation déclarée "à prise en compte immédiate"
# Les évaluations de rattrapage et 2eme session sont toujours complètes # 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. etudids_sans_note = inscrits_module - set(eval_df.index) # sans les dem.
is_complete = ( is_complete = (
(evaluation.evaluation_type != Evaluation.EVALUATION_NORMALE) (evaluation.evaluation_type != Evaluation.EVALUATION_NORMALE)
or (evaluation.publish_incomplete) or (evaluation.publish_incomplete)
or (not etudids_sans_note) or (not etudids_sans_note)
) ) and not evaluation.is_blocked()
self.evaluations_completes.append(is_complete) self.evaluations_completes.append(is_complete)
self.evaluations_completes_dict[evaluation.id] = is_complete self.evaluations_completes_dict[evaluation.id] = is_complete
self.evals_etudids_sans_note[evaluation.id] = etudids_sans_note self.evals_etudids_sans_note[evaluation.id] = etudids_sans_note
@ -185,7 +186,7 @@ class ModuleImplResults:
].index ].index
) )
if evaluation.publish_incomplete: 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 eval_etudids_attente |= etudids_sans_note
# Synthèse pour état du module: # Synthèse pour état du module:
self.etudids_attente |= eval_etudids_attente self.etudids_attente |= eval_etudids_attente
@ -276,7 +277,7 @@ class ModuleImplResults:
) / [e.note_max / 20.0 for e in moduleimpl.evaluations] ) / [e.note_max / 20.0 for e in moduleimpl.evaluations]
def get_eval_notes_dict(self, evaluation_id: int) -> dict: 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 } { etudid : valeur }
avec les valeurs float, ou "ABS" ou EXC avec les valeurs float, ou "ABS" ou EXC
""" """

View File

@ -230,8 +230,8 @@ class ResultatsSemestre(ResultatsCache):
date_modif = cursor.one_or_none() date_modif = cursor.one_or_none()
last_modif = date_modif[0] if date_modif else None last_modif = date_modif[0] if date_modif else None
return { return {
"coefficient": evaluation.coefficient or 0.0, "coefficient": evaluation.coefficient,
"description": evaluation.description or "", "description": evaluation.description,
"evaluation_id": evaluation.id, "evaluation_id": evaluation.id,
"jour": evaluation.date_debut or datetime.datetime(1900, 1, 1), "jour": evaluation.date_debut or datetime.datetime(1900, 1, 1),
"etat": { "etat": {

View File

@ -10,6 +10,7 @@ from flask_login import current_user
import sqlalchemy as sa import sqlalchemy as sa
from app import db, log from app import db, log
from app import models
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.events import ScolarNews from app.models.events import ScolarNews
from app.models.notes import NotesNotes from app.models.notes import NotesNotes
@ -24,7 +25,7 @@ NOON = datetime.time(12, 00)
DEFAULT_EVALUATION_TIME = datetime.time(8, 0) DEFAULT_EVALUATION_TIME = datetime.time(8, 0)
class Evaluation(db.Model): class Evaluation(models.ScoDocModel):
"""Evaluation (contrôle, examen, ...)""" """Evaluation (contrôle, examen, ...)"""
__tablename__ = "notes_evaluation" __tablename__ = "notes_evaluation"
@ -36,9 +37,9 @@ class Evaluation(db.Model):
) )
date_debut = db.Column(db.DateTime(timezone=True), nullable=True) date_debut = db.Column(db.DateTime(timezone=True), nullable=True)
date_fin = db.Column(db.DateTime(timezone=True), nullable=True) date_fin = db.Column(db.DateTime(timezone=True), nullable=True)
description = db.Column(db.Text) description = db.Column(db.Text, nullable=False)
note_max = db.Column(db.Float) note_max = db.Column(db.Float, nullable=False)
coefficient = db.Column(db.Float) coefficient = db.Column(db.Float, nullable=False)
visibulletin = db.Column( visibulletin = db.Column(
db.Boolean, nullable=False, default=True, server_default="true" db.Boolean, nullable=False, default=True, server_default="true"
) )
@ -46,10 +47,14 @@ class Evaluation(db.Model):
publish_incomplete = db.Column( publish_incomplete = db.Column(
db.Boolean, nullable=False, default=False, server_default="false" 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( evaluation_type = db.Column(
db.Integer, nullable=False, default=0, server_default="0" 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 # ordre de presentation (par défaut, le plus petit numero
# est la plus ancienne eval): # est la plus ancienne eval):
numero = db.Column(db.Integer, nullable=False, default=0) numero = db.Column(db.Integer, nullable=False, default=0)
@ -79,6 +84,7 @@ class Evaluation(db.Model):
date_fin: datetime.datetime = None, date_fin: datetime.datetime = None,
description=None, description=None,
note_max=None, note_max=None,
blocked_until=None,
coefficient=None, coefficient=None,
visibulletin=None, visibulletin=None,
publish_incomplete=None, publish_incomplete=None,
@ -208,6 +214,10 @@ class Evaluation(db.Model):
def to_dict_api(self) -> dict: def to_dict_api(self) -> dict:
"Représentation dict pour API JSON" "Représentation dict pour API JSON"
return { return {
"blocked": self.is_blocked(),
"blocked_until": (
self.blocked_until.isoformat() if self.blocked_until else ""
),
"coefficient": self.coefficient, "coefficient": self.coefficient,
"date_debut": self.date_debut.isoformat() if self.date_debut else "", "date_debut": self.date_debut.isoformat() if self.date_debut else "",
"date_fin": self.date_fin.isoformat() if self.date_fin else "", "date_fin": self.date_fin.isoformat() if self.date_fin else "",
@ -244,14 +254,14 @@ class Evaluation(db.Model):
return e_dict return e_dict
def from_dict(self, data): def convert_dict_fields(self, args: dict) -> dict:
"""Set evaluation attributes from given dict values.""" """Convert fields in the given dict. No other side effect.
check_convert_evaluation_args(self.moduleimpl, data) returns: dict to store in model's db.
if data.get("numero") is None: """
data["numero"] = Evaluation.get_max_numero(self.moduleimpl.id) + 1 check_convert_evaluation_args(self.moduleimpl, args)
for k in self.__dict__: if args.get("numero") is None:
if k != "_sa_instance_state" and k != "id" and k in data: args["numero"] = Evaluation.get_max_numero(self.moduleimpl.id) + 1
setattr(self, k, data[k]) return args
@classmethod @classmethod
def get_evaluation( def get_evaluation(
@ -370,19 +380,6 @@ class Evaluation(db.Model):
Chaine vide si non renseignée.""" Chaine vide si non renseignée."""
return self.date_fin.time().isoformat("minutes") if self.date_fin else "" 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: def is_matin(self) -> bool:
"Evaluation commençant le matin (faux si pas de date)" "Evaluation commençant le matin (faux si pas de date)"
if not self.date_debut: if not self.date_debut:
@ -395,6 +392,14 @@ class Evaluation(db.Model):
return False return False
return self.date_debut.time() >= NOON 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: def set_default_poids(self) -> bool:
"""Initialize les poids vers les UE à leurs valeurs par défaut """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. 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 !", "Heures de l'évaluation incohérentes !",
dest_url="javascript:history.back();", 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: def heure_to_time(heure: str) -> datetime.time:

View File

@ -93,6 +93,10 @@ class FormSemestre(db.Model):
db.Boolean(), nullable=False, default=False, server_default="false" db.Boolean(), nullable=False, default=False, server_default="false"
) )
"Si vrai, la moyenne générale indicative BUT n'est pas calculée" "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( gestion_semestrielle = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false" db.Boolean(), nullable=False, default=False, server_default="false"
) )

View File

@ -318,7 +318,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
if nt.bonus_ues is not None: if nt.bonus_ues is not None:
u["cur_moy_ue_txt"] += " (+ues)" u["cur_moy_ue_txt"] += " (+ues)"
u["moy_ue_txt"] = scu.fmt_note(ue_status["moy"]) 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"]) u["coef_ue_txt"] = scu.fmt_coef(ue_status["coef_ue"])
else: else:
u["coef_ue_txt"] = "-" u["coef_ue_txt"] = "-"
@ -558,6 +558,8 @@ def _ue_mod_bulletin(
).order_by(Evaluation.numero, Evaluation.date_debut) ).order_by(Evaluation.numero, Evaluation.date_debut)
# (plus ancienne d'abord) # (plus ancienne d'abord)
for e in all_evals: for e in all_evals:
if e.is_blocked():
continue # ignore évaluations bloquées
if not e.visibulletin and version != "long": if not e.visibulletin and version != "long":
continue continue
is_complete = e.id in complete_eval_ids is_complete = e.id in complete_eval_ids
@ -625,7 +627,7 @@ def _ue_mod_bulletin(
) )
): ):
# ne liste pas les eval malus sans notes # 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: if e.id in complete_eval_ids:
mod["evaluations"].append(e_dict) mod["evaluations"].append(e_dict)
else: else:

View File

@ -25,7 +25,7 @@
# #
############################################################################## ##############################################################################
"""Génération du bulletin en format JSON """Génération du bulletin en format JSON (formations classiques)
""" """
import datetime import datetime

View File

@ -108,7 +108,7 @@ def evaluation_create_form(
raise ValueError("missing evaluation_id parameter") raise ValueError("missing evaluation_id parameter")
initvalues = evaluation.to_dict() initvalues = evaluation.to_dict()
moduleimpl_id = initvalues["moduleimpl_id"] moduleimpl_id = initvalues["moduleimpl_id"]
submitlabel = "Modifier les données" submitlabel = "Modifier l'évaluation"
action = "Modification d'une évaluation" action = "Modification d'une évaluation"
link = "" link = ""
# Note maximale actuelle dans cette éval ? # Note maximale actuelle dans cette éval ?
@ -142,6 +142,15 @@ def evaluation_create_form(
else: else:
poids = 0.0 poids = 0.0
initvalues[f"poids_{ue.id}"] = poids 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 = [ form = [
("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}), ("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). "explanation": """importance de l'évaluation (multiplie les poids ci-dessous).
Non utilisé pour les bonus.""", Non utilisé pour les bonus.""",
"allow_null": False, "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( tf = TrivialFormulator(
request.base_url, request.base_url,
vals, vals,
@ -331,7 +363,9 @@ def evaluation_create_form(
+ "\n".join(H) + "\n".join(H)
+ "\n" + "\n"
+ tf[1] + 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") + render_template("sco_timepicker.j2")
+ html_sco_header.sco_footer() + html_sco_header.sco_footer()
) )
@ -357,7 +391,8 @@ def evaluation_create_form(
raise ScoValueError("Heure début invalide") from exc raise ScoValueError("Heure début invalide") from exc
args["date_debut"] = datetime.datetime.combine(date_debut, heure_debut) args["date_debut"] = datetime.datetime.combine(date_debut, heure_debut)
args.pop("heure_debut", None) 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"): if date_debut and args.get("heure_fin"):
try: try:
heure_fin = heure_to_time(args["heure_fin"]) heure_fin = heure_to_time(args["heure_fin"])
@ -365,6 +400,19 @@ def evaluation_create_form(
raise ScoValueError("Heure fin invalide") from exc raise ScoValueError("Heure fin invalide") from exc
args["date_fin"] = datetime.datetime.combine(date_debut, heure_fin) args["date_fin"] = datetime.datetime.combine(date_debut, heure_fin)
args.pop("heure_fin", None) 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: if edit:
evaluation.from_dict(args) evaluation.from_dict(args)

View File

@ -40,7 +40,7 @@ from app import db
from app.auth.models import User from app.auth.models import User
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat 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 import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType 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 html_sco_header
from app.scodoc import sco_cal from app.scodoc import sco_cal
from app.scodoc import sco_evaluation_db 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_formsemestre_inscriptions
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
@ -113,6 +112,7 @@ def do_evaluation_etat(
nb_neutre, nb_neutre,
nb_att, nb_att,
moy, median, mini, maxi : # notes, en chaine, sur 20 moy, median, mini, maxi : # notes, en chaine, sur 20
maxi_num : note max, numérique
last_modif: datetime, * last_modif: datetime, *
gr_complets, gr_incomplets, gr_complets, gr_incomplets,
evalcomplete * evalcomplete *
@ -129,11 +129,12 @@ def do_evaluation_etat(
) # { etudid : note } ) # { etudid : note }
# ---- Liste des groupes complets et incomplets # ---- Liste des groupes complets et incomplets
E = sco_evaluation_db.get_evaluations_dict(args={"evaluation_id": evaluation_id})[0] evaluation = Evaluation.get_evaluation(evaluation_id)
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] modimpl: ModuleImpl = evaluation.moduleimpl
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] module: Module = modimpl.module
is_malus = Mod["module_type"] == ModuleType.MALUS # True si module de malus
formsemestre_id = M["formsemestre_id"] 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: # Si partition_id is None, prend 'all' ou bien la premiere:
if partition_id is None: if partition_id is None:
if select_first_partition: if select_first_partition:
@ -149,9 +150,7 @@ def do_evaluation_etat(
insem = sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits( insem = sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits(
formsemestre_id formsemestre_id
) )
insmod = sco_moduleimpl.do_moduleimpl_inscription_list( insmod = sco_moduleimpl.do_moduleimpl_inscription_list(moduleimpl_id=modimpl.id)
moduleimpl_id=E["moduleimpl_id"]
)
insmodset = {x["etudid"] for x in insmod} insmodset = {x["etudid"] for x in insmod}
# retire de insem ceux qui ne sont pas inscrits au module # retire de insem ceux qui ne sont pas inscrits au module
ins = [i for i in insem if i["etudid"] in insmodset] ins = [i for i in insem if i["etudid"] in insmodset]
@ -174,9 +173,9 @@ def do_evaluation_etat(
maxi_num = None maxi_num = None
else: else:
median = scu.fmt_note(median_num) median = scu.fmt_note(median_num)
moy = scu.fmt_note(moy_num, E["note_max"]) moy = scu.fmt_note(moy_num, evaluation.note_max)
mini = scu.fmt_note(mini_num, E["note_max"]) mini = scu.fmt_note(mini_num, evaluation.note_max)
maxi = scu.fmt_note(maxi_num, E["note_max"]) maxi = scu.fmt_note(maxi_num, evaluation.note_max)
# cherche date derniere modif note # cherche date derniere modif note
if len(etuds_notes_dict): if len(etuds_notes_dict):
t = [x["date"] for x in etuds_notes_dict.values()] 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 = list(group_nb_missing.keys())
gr_incomplets.sort() gr_incomplets.sort()
complete = (total_nb_missing == 0) or ( complete = (
E["evaluation_type"] != Evaluation.EVALUATION_NORMALE (total_nb_missing == 0)
or (evaluation.evaluation_type != Evaluation.EVALUATION_NORMALE)
and not evaluation.is_blocked()
) )
evalattente = (total_nb_missing > 0) and ( 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: # 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 evalattente = False
# Calcul moyenne dans chaque groupe de TD # Calcul moyenne dans chaque groupe de TD
@ -236,10 +237,10 @@ def do_evaluation_etat(
{ {
"group_id": group_id, "group_id": group_id,
"group_name": group_by_id[group_id]["group_name"], "group_name": group_by_id[group_id]["group_name"],
"gr_moy": scu.fmt_note(gr_moy, E["note_max"]), "gr_moy": scu.fmt_note(gr_moy, evaluation.note_max),
"gr_median": scu.fmt_note(gr_median, E["note_max"]), "gr_median": scu.fmt_note(gr_median, evaluation.note_max),
"gr_mini": scu.fmt_note(gr_mini, E["note_max"]), "gr_mini": scu.fmt_note(gr_mini, evaluation.note_max),
"gr_maxi": scu.fmt_note(gr_maxi, E["note_max"]), "gr_maxi": scu.fmt_note(gr_maxi, evaluation.note_max),
"gr_nb_notes": len(notes), "gr_nb_notes": len(notes),
"gr_nb_att": len([x for x in notes if x == scu.NOTES_ATTENTE]), "gr_nb_att": len([x for x in notes if x == scu.NOTES_ATTENTE]),
} }

View File

@ -534,7 +534,7 @@ def excel_feuille_saisie(evaluation: "Evaluation", titreannee, description, line
# description evaluation # description evaluation
ws.append_single_cell_row(scu.unescape_html(description), style_titres) ws.append_single_cell_row(scu.unescape_html(description), style_titres)
ws.append_single_cell_row( 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, style,
) )
# ligne blanche # ligne blanche

View File

@ -531,6 +531,10 @@ def _ligne_evaluation(
if not evaluation.visibulletin: if not evaluation.visibulletin:
tr_class += " non_visible_inter" tr_class += " non_visible_inter"
tr_class_1 = "mievr" tr_class_1 = "mievr"
if evaluation.is_blocked():
tr_class += " evaluation_blocked"
tr_class_1 += " evaluation_blocked"
if not first_eval: if not first_eval:
H.append("""<tr><td colspan="8">&nbsp;</td></tr>""") H.append("""<tr><td colspan="8">&nbsp;</td></tr>""")
tr_class_1 += " mievr_spaced" tr_class_1 += " mievr_spaced"
@ -564,7 +568,7 @@ def _ligne_evaluation(
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id) scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id)
}" class="mievr_evalnodate">Évaluation sans date</a>""" }" class="mievr_evalnodate">Évaluation sans date</a>"""
) )
H.append(f"&nbsp;&nbsp;&nbsp; <em>{evaluation.description or ''}</em>") H.append(f"&nbsp;&nbsp;&nbsp; <em>{evaluation.description}</em>")
if evaluation.evaluation_type == Evaluation.EVALUATION_RATTRAPAGE: if evaluation.evaluation_type == Evaluation.EVALUATION_RATTRAPAGE:
H.append( H.append(
"""<span class="mievr_rattr" title="remplace si meilleure note">rattrapage</span>""" """<span class="mievr_rattr" title="remplace si meilleure note">rattrapage</span>"""
@ -611,8 +615,15 @@ def _ligne_evaluation(
else: else:
H.append(arrow_none) H.append(arrow_none)
if etat["evalcomplete"]: if evaluation.is_blocked():
etat_txt = f"""(prise en compte{ etat_txt = f"""évaluation bloquée {
"jusqu'au " + evaluation.blocked_until.strftime("%d/%m/%Y")
if evaluation.blocked_until < Evaluation.BLOCKED_FOREVER
else "" }
"""
etat_descr = """prise en compte bloquée"""
elif etat["evalcomplete"]:
etat_txt = f"""Moyenne (prise en compte{
"" ""
if evaluation.visibulletin if evaluation.visibulletin
else ", cachée en intermédiaire"}) else ", cachée en intermédiaire"})
@ -621,7 +632,7 @@ def _ligne_evaluation(
", évaluation cachée sur les bulletins en version intermédiaire et sur la passerelle" ", évaluation cachée sur les bulletins en version intermédiaire et sur la passerelle"
}""" }"""
elif etat["evalattente"] and not evaluation.publish_incomplete: elif etat["evalattente"] and not evaluation.publish_incomplete:
etat_txt = "(prise en compte, mais <b>notes en attente</b>)" etat_txt = "Moyenne (prise en compte, mais <b>notes en attente</b>)"
etat_descr = "il y a des notes en attente" etat_descr = "il y a des notes en attente"
elif evaluation.publish_incomplete: elif evaluation.publish_incomplete:
etat_txt = """(prise en compte <b>immédiate</b>)""" etat_txt = """(prise en compte <b>immédiate</b>)"""
@ -629,11 +640,12 @@ def _ligne_evaluation(
"il manque des notes, mais la prise en compte immédiate a été demandée" "il manque des notes, mais la prise en compte immédiate a été demandée"
) )
elif etat["nb_notes"] != 0: elif etat["nb_notes"] != 0:
etat_txt = "(<b>non</b> prise en compte)" etat_txt = "Moyenne (<b>non</b> prise en compte)"
etat_descr = "il manque des notes" etat_descr = "il manque des notes"
else: else:
etat_txt = "" etat_txt = ""
if can_edit_evals and etat_txt: if etat_txt:
if can_edit_evals:
etat_txt = f"""<a href="{ url_for("notes.evaluation_edit", etat_txt = f"""<a href="{ url_for("notes.evaluation_edit",
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id) scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id)
}" title="{etat_descr}">{etat_txt}</a>""" }" title="{etat_descr}">{etat_txt}</a>"""
@ -641,16 +653,16 @@ def _ligne_evaluation(
H.append( H.append(
f"""</span></span></td> f"""</span></span></td>
</tr> </tr>
<tr class="{tr_class}"> <tr class="{tr_class} mievr_in">
<th class="moduleimpl_evaluations" colspan="2">&nbsp;</th> <th class="moduleimpl_evaluations" colspan="2">&nbsp;</th>
<th class="moduleimpl_evaluations">Durée</th> <th class="moduleimpl_evaluations">Durée</th>
<th class="moduleimpl_evaluations">Coef.</th> <th class="moduleimpl_evaluations">Coef.</th>
<th class="moduleimpl_evaluations">Notes</th> <th class="moduleimpl_evaluations">Notes</th>
<th class="moduleimpl_evaluations">Abs</th> <th class="moduleimpl_evaluations">Abs</th>
<th class="moduleimpl_evaluations">N</th> <th class="moduleimpl_evaluations">N</th>
<th class="moduleimpl_evaluations" colspan="2">Moyenne {etat_txt}</th> <th class="moduleimpl_evaluations moduleimpl_evaluation_moy" colspan="2"><span>{etat_txt}</span></th>
</tr> </tr>
<tr class="{tr_class}"> <tr class="{tr_class} mievr_in">
<td class="mievr">""" <td class="mievr">"""
) )
if can_edit_evals: if can_edit_evals:
@ -832,7 +844,7 @@ def _evaluation_poids_html(evaluation: Evaluation, max_poids: float = 0.0) -> st
+ "\n".join( + "\n".join(
[ [
f"""<div title="poids vers {ue.acronyme}: {poids:g}"> f"""<div title="poids vers {ue.acronyme}: {poids:g}">
<div style="--size:{math.sqrt(poids*(evaluation.coefficient or 0.)/max_poids*144)}px; <div style="--size:{math.sqrt(poids*(evaluation.coefficient)/max_poids*144)}px;
{'background-color: ' + ue.color + ';' if ue.color else ''} {'background-color: ' + ue.color + ';' if ue.color else ''}
"></div> "></div>
</div>""" </div>"""

View File

@ -884,7 +884,7 @@ def feuille_saisie_notes(evaluation_id, group_ids=[]):
if evaluation.date_debut: if evaluation.date_debut:
indication_date = evaluation.date_debut.date().isoformat() indication_date = evaluation.date_debut.date().isoformat()
else: else:
indication_date = scu.sanitize_filename(evaluation.description or "")[:12] indication_date = scu.sanitize_filename(evaluation.description)[:12]
eval_name = f"{evaluation.moduleimpl.module.code}-{indication_date}" eval_name = f"{evaluation.moduleimpl.module.code}-{indication_date}"
date_str = ( date_str = (

View File

@ -1469,6 +1469,9 @@ span.eval_title {
font-size: 14pt; font-size: 14pt;
} }
#evaluation-edit-blocked td, #evaluation-edit-coef td {
padding-top: 24px;
}
/* #saisie_notes span.eval_title { /* #saisie_notes span.eval_title {
border-bottom: 1px solid rgb(100,100,100); border-bottom: 1px solid rgb(100,100,100);
} }
@ -2099,6 +2102,14 @@ th.moduleimpl_evaluations a:hover {
text-decoration: underline; text-decoration: underline;
} }
tr.mievr_in.evaluation_blocked th.moduleimpl_evaluation_moy span, tr.evaluation_blocked th.moduleimpl_evaluation_moy a {
font-weight: bold;
color: red;
background-color: yellow;
padding: 2px;
border-radius: 2px;
}
tr.mievr { tr.mievr {
background-color: #eeeeee; background-color: #eeeeee;
} }
@ -2153,6 +2164,15 @@ tr.mievr.non_visible_inter th {
); );
} }
tr.mievr_tit.evaluation_blocked td,tr.mievr_tit.evaluation_blocked th {
background-image: radial-gradient(#bd7777 1px, transparent 1px);
background-size: 10px 10px;
}
tr.mievr_in.evaluation_blocked td, tr.mievr_in.evaluation_blocked th {
background-color: rgb(195, 235, 255);
}
tr.mievr th { tr.mievr th {
background-color: white; background-color: white;
} }
@ -2163,6 +2183,7 @@ tr.mievr td.mievr {
tr.mievr td.mievr_menu { tr.mievr td.mievr_menu {
width: 110px; width: 110px;
padding-bottom: 4px;
} }
tr.mievr td.mievr_dur { tr.mievr td.mievr_dur {

View File

@ -457,7 +457,7 @@ class TableRecap(tb.Table):
row_descr_eval.add_cell( row_descr_eval.add_cell(
col_id, col_id,
None, None,
e.description or "", e.description,
target=url_for( target=url_for(
"notes.evaluation_listenotes", "notes.evaluation_listenotes",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,

View File

@ -20,7 +20,7 @@ Assiduité lors de l'évaluation
<a class="stdlink" href="{{ <a class="stdlink" href="{{
url_for('notes.evaluation_listenotes', url_for('notes.evaluation_listenotes',
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id) scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id)
}}"><em>{{evaluation.description or ''}}</em></a> }}"><em>{{evaluation.description}}</em></a>
{% endif %} {% endif %}
<a style="margin-left:32px;" href="{{request.url}}&fmt=xlsx">{{scu.ICON_XLS|safe}}</a> <a style="margin-left:32px;" href="{{request.url}}&fmt=xlsx">{{scu.ICON_XLS|safe}}</a>
</div> </div>

View File

@ -1642,7 +1642,7 @@ def evaluation_delete(evaluation_id):
.first_or_404() .first_or_404()
) )
tit = f"""Suppression de l'évaluation {evaluation.description or ""} ({evaluation.descr_date()})""" tit = f"""Suppression de l'évaluation {evaluation.description} ({evaluation.descr_date()})"""
etat = sco_evaluations.do_evaluation_etat(evaluation.id) etat = sco_evaluations.do_evaluation_etat(evaluation.id)
H = [ H = [
f""" f"""

View File

@ -0,0 +1,83 @@
"""evaluation bloquee
Revision ID: cddabc3f868a
Revises: 2e4875004e12
Create Date: 2024-02-25 16:39:45.947342
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.orm import sessionmaker # added by ev
# revision identifiers, used by Alembic.
revision = "cddabc3f868a"
down_revision = "2e4875004e12"
branch_labels = None
depends_on = None
Session = sessionmaker()
def upgrade():
# ces champs étaient nullables
# Added by ev: remove duplicates
bind = op.get_bind()
session = Session(bind=bind)
session.execute(
sa.text(
"""UPDATE notes_evaluation SET description='' WHERE description IS NULL;"""
)
)
session.execute(
sa.text("""UPDATE notes_evaluation SET note_max=20. WHERE note_max IS NULL;""")
)
session.execute(
sa.text(
"""UPDATE notes_evaluation SET coefficient=0. WHERE coefficient IS NULL;"""
)
)
#
with op.batch_alter_table("notes_evaluation", schema=None) as batch_op:
batch_op.add_column(
sa.Column("blocked_until", sa.DateTime(timezone=True), nullable=True)
)
batch_op.alter_column("description", existing_type=sa.TEXT(), nullable=False)
batch_op.alter_column(
"note_max", existing_type=sa.DOUBLE_PRECISION(precision=53), nullable=False
)
batch_op.alter_column(
"coefficient",
existing_type=sa.DOUBLE_PRECISION(precision=53),
nullable=False,
)
with op.batch_alter_table("notes_formsemestre", schema=None) as batch_op:
batch_op.add_column(
sa.Column(
"mode_calcul_moyennes", sa.Integer(), server_default="0", nullable=False
)
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("notes_formsemestre", schema=None) as batch_op:
batch_op.drop_column("mode_calcul_moyennes")
with op.batch_alter_table("notes_evaluation", schema=None) as batch_op:
batch_op.alter_column(
"coefficient",
existing_type=sa.DOUBLE_PRECISION(precision=53),
nullable=True,
)
batch_op.alter_column(
"note_max", existing_type=sa.DOUBLE_PRECISION(precision=53), nullable=True
)
batch_op.alter_column("description", existing_type=sa.TEXT(), nullable=True)
batch_op.drop_column("blocked_until")
# ### end Alembic commands ###