Tableau bord module: avertissement si poids d'évaluation nuls. Début de #411.

This commit is contained in:
Emmanuel Viennet 2022-10-01 18:55:32 +02:00 committed by iziram
parent c0f3f1543f
commit a5dd1967fc
3 changed files with 1046 additions and 981 deletions

View File

@ -1,250 +1,263 @@
# -*- coding: UTF-8 -* # -*- coding: UTF-8 -*
"""ScoDoc models: evaluations """ScoDoc models: evaluations
""" """
import datetime import datetime
from app import db from app import db
from app.models.moduleimpls import ModuleImpl from app.models.moduleimpls import ModuleImpl
from app.models.ues import UniteEns from app.models.ues import UniteEns
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
class Evaluation(db.Model): class Evaluation(db.Model):
"""Evaluation (contrôle, examen, ...)""" """Evaluation (contrôle, examen, ...)"""
__tablename__ = "notes_evaluation" __tablename__ = "notes_evaluation"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
evaluation_id = db.synonym("id") evaluation_id = db.synonym("id")
moduleimpl_id = db.Column( moduleimpl_id = db.Column(
db.Integer, db.ForeignKey("notes_moduleimpl.id"), index=True db.Integer, db.ForeignKey("notes_moduleimpl.id"), index=True
) )
jour = db.Column(db.Date) jour = db.Column(db.Date)
heure_debut = db.Column(db.Time) heure_debut = db.Column(db.Time)
heure_fin = db.Column(db.Time) heure_fin = db.Column(db.Time)
description = db.Column(db.Text) description = db.Column(db.Text)
note_max = db.Column(db.Float) note_max = db.Column(db.Float)
coefficient = db.Column(db.Float) coefficient = db.Column(db.Float)
visibulletin = db.Column( visibulletin = db.Column(
db.Boolean, nullable=False, default=True, server_default="true" db.Boolean, nullable=False, default=True, server_default="true"
) )
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" # type d'evaluation: 0 normale, 1 rattrapage, 2 "2eme session"
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"
) )
# 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) numero = db.Column(db.Integer)
ues = db.relationship("UniteEns", secondary="evaluation_ue_poids", viewonly=True) ues = db.relationship("UniteEns", secondary="evaluation_ue_poids", viewonly=True)
def __repr__(self): def __repr__(self):
return f"""<Evaluation {self.id} {self.jour.isoformat() if self.jour else ''} "{self.description[:16] if self.description else ''}">""" return f"""<Evaluation {self.id} {
self.jour.isoformat() if self.jour else ''} "{
def to_dict(self) -> dict: self.description[:16] if self.description else ''}">"""
"Représentation dict, pour json"
e = dict(self.__dict__) def to_dict(self) -> dict:
e.pop("_sa_instance_state", None) "Représentation dict, pour json"
# ScoDoc7 output_formators e = dict(self.__dict__)
e["evaluation_id"] = self.id e.pop("_sa_instance_state", None)
e["jour"] = e["jour"].strftime("%d/%m/%Y") if e["jour"] else "" # ScoDoc7 output_formators
if self.jour is None: e["evaluation_id"] = self.id
e["date_debut"] = None e["jour"] = e["jour"].strftime("%d/%m/%Y") if e["jour"] else ""
e["date_fin"] = None if self.jour is None:
else: e["date_debut"] = None
e["date_debut"] = datetime.datetime.combine( e["date_fin"] = None
self.jour, self.heure_debut or datetime.time(0, 0) else:
).isoformat() e["date_debut"] = datetime.datetime.combine(
e["date_fin"] = datetime.datetime.combine( self.jour, self.heure_debut or datetime.time(0, 0)
self.jour, self.heure_fin or datetime.time(0, 0) ).isoformat()
).isoformat() e["date_fin"] = datetime.datetime.combine(
e["numero"] = ndb.int_null_is_zero(e["numero"]) self.jour, self.heure_fin or datetime.time(0, 0)
e["poids"] = self.get_ue_poids_dict() # { ue_id : poids } ).isoformat()
return evaluation_enrich_dict(e) e["numero"] = ndb.int_null_is_zero(e["numero"])
e["poids"] = self.get_ue_poids_dict() # { ue_id : poids }
def from_dict(self, data): return evaluation_enrich_dict(e)
"""Set evaluation attributes from given dict values."""
check_evaluation_args(data) def from_dict(self, data):
for k in self.__dict__.keys(): """Set evaluation attributes from given dict values."""
if k != "_sa_instance_state" and k != "id" and k in data: check_evaluation_args(data)
setattr(self, k, data[k]) for k in self.__dict__.keys():
if k != "_sa_instance_state" and k != "id" and k in data:
def clone(self, not_copying=()): setattr(self, k, data[k])
"""Clone, not copying the given attrs
Attention: la copie n'a pas d'id avant le prochain commit def descr_heure(self) -> str:
""" "Description de la plage horaire pour affichages"
d = dict(self.__dict__) if self.heure_debut and (
d.pop("id") # get rid of id not self.heure_fin or self.heure_fin == self.heure_debut
d.pop("_sa_instance_state") # get rid of SQLAlchemy special attr ):
for k in not_copying: return f"""à {self.heure_debut.strftime("%H:%M")}"""
d.pop(k) elif self.heure_debut and self.heure_fin:
copy = self.__class__(**d) return f"""de {self.heure_debut.strftime("%H:%M")} à {self.heure_fin.strftime("%H:%M")}"""
db.session.add(copy) else:
return copy return ""
def set_ue_poids(self, ue, poids: float) -> None: def clone(self, not_copying=()):
"""Set poids évaluation vers cette UE""" """Clone, not copying the given attrs
self.update_ue_poids_dict({ue.id: poids}) Attention: la copie n'a pas d'id avant le prochain commit
"""
def set_ue_poids_dict(self, ue_poids_dict: dict) -> None: d = dict(self.__dict__)
"""set poids vers les UE (remplace existants) d.pop("id") # get rid of id
ue_poids_dict = { ue_id : poids } d.pop("_sa_instance_state") # get rid of SQLAlchemy special attr
""" for k in not_copying:
L = [] d.pop(k)
for ue_id, poids in ue_poids_dict.items(): copy = self.__class__(**d)
ue = UniteEns.query.get(ue_id) db.session.add(copy)
L.append(EvaluationUEPoids(evaluation=self, ue=ue, poids=poids)) return copy
self.ue_poids = L
self.moduleimpl.invalidate_evaluations_poids() # inval cache def set_ue_poids(self, ue, poids: float) -> None:
"""Set poids évaluation vers cette UE"""
def update_ue_poids_dict(self, ue_poids_dict: dict) -> None: self.update_ue_poids_dict({ue.id: poids})
"""update poids vers UE (ajoute aux existants)"""
current = self.get_ue_poids_dict() def set_ue_poids_dict(self, ue_poids_dict: dict) -> None:
current.update(ue_poids_dict) """set poids vers les UE (remplace existants)
self.set_ue_poids_dict(current) ue_poids_dict = { ue_id : poids }
"""
def get_ue_poids_dict(self) -> dict: L = []
"""returns { ue_id : poids }""" for ue_id, poids in ue_poids_dict.items():
return {p.ue.id: p.poids for p in self.ue_poids} ue = UniteEns.query.get(ue_id)
L.append(EvaluationUEPoids(evaluation=self, ue=ue, poids=poids))
def get_ue_poids_str(self) -> str: self.ue_poids = L
"""string describing poids, for excel cells and pdfs self.moduleimpl.invalidate_evaluations_poids() # inval cache
Note: si les poids ne sont pas initialisés (poids par défaut),
ils ne sont pas affichés. def update_ue_poids_dict(self, ue_poids_dict: dict) -> None:
""" """update poids vers UE (ajoute aux existants)"""
# restreint aux UE du semestre dans lequel est cette évaluation current = self.get_ue_poids_dict()
# au cas où le module ait changé de semestre et qu'il reste des poids current.update(ue_poids_dict)
evaluation_semestre_idx = self.moduleimpl.module.semestre_id self.set_ue_poids_dict(current)
return ", ".join(
[ def get_ue_poids_dict(self) -> dict:
f"{p.ue.acronyme}: {p.poids}" """returns { ue_id : poids }"""
for p in sorted( return {p.ue.id: p.poids for p in self.ue_poids}
self.ue_poids, key=lambda p: (p.ue.numero or 0, p.ue.acronyme)
) def get_ue_poids_str(self) -> str:
if evaluation_semestre_idx == p.ue.semestre_idx """string describing poids, for excel cells and pdfs
] Note: si les poids ne sont pas initialisés (poids par défaut),
) ils ne sont pas affichés.
"""
# restreint aux UE du semestre dans lequel est cette évaluation
class EvaluationUEPoids(db.Model): # au cas où le module ait changé de semestre et qu'il reste des poids
"""Poids des évaluations (BUT) evaluation_semestre_idx = self.moduleimpl.module.semestre_id
association many to many return ", ".join(
""" [
f"{p.ue.acronyme}: {p.poids}"
evaluation_id = db.Column( for p in sorted(
db.Integer, self.ue_poids, key=lambda p: (p.ue.numero or 0, p.ue.acronyme)
db.ForeignKey("notes_evaluation.id", ondelete="CASCADE"), )
primary_key=True, if evaluation_semestre_idx == p.ue.semestre_idx
) ]
ue_id = db.Column( )
db.Integer,
db.ForeignKey("notes_ue.id", ondelete="CASCADE"),
primary_key=True, class EvaluationUEPoids(db.Model):
) """Poids des évaluations (BUT)
poids = db.Column( association many to many
db.Float, """
nullable=False,
) evaluation_id = db.Column(
evaluation = db.relationship( db.Integer,
Evaluation, db.ForeignKey("notes_evaluation.id", ondelete="CASCADE"),
backref=db.backref("ue_poids", cascade="all, delete-orphan"), primary_key=True,
) )
ue = db.relationship( ue_id = db.Column(
UniteEns, db.Integer,
backref=db.backref("evaluation_ue_poids", cascade="all, delete-orphan"), db.ForeignKey("notes_ue.id", ondelete="CASCADE"),
) primary_key=True,
)
def __repr__(self): poids = db.Column(
return f"<EvaluationUEPoids {self.evaluation} {self.ue} poids={self.poids}>" db.Float,
nullable=False,
)
# Fonction héritée de ScoDoc7 à refactorer evaluation = db.relationship(
def evaluation_enrich_dict(e): Evaluation,
"""add or convert some fields in an evaluation dict""" backref=db.backref("ue_poids", cascade="all, delete-orphan"),
# For ScoDoc7 compat )
heure_debut_dt = e["heure_debut"] or datetime.time( ue = db.relationship(
8, 00 UniteEns,
) # au cas ou pas d'heure (note externe?) backref=db.backref("evaluation_ue_poids", cascade="all, delete-orphan"),
heure_fin_dt = e["heure_fin"] or datetime.time(8, 00) )
e["heure_debut"] = ndb.TimefromISO8601(e["heure_debut"])
e["heure_fin"] = ndb.TimefromISO8601(e["heure_fin"]) def __repr__(self):
e["jouriso"] = ndb.DateDMYtoISO(e["jour"]) return f"<EvaluationUEPoids {self.evaluation} {self.ue} poids={self.poids}>"
heure_debut, heure_fin = e["heure_debut"], e["heure_fin"]
d = ndb.TimeDuration(heure_debut, heure_fin)
if d is not None: # Fonction héritée de ScoDoc7 à refactorer
m = d % 60 def evaluation_enrich_dict(e: dict):
e["duree"] = "%dh" % (d / 60) """add or convert some fields in an evaluation dict"""
if m != 0: # For ScoDoc7 compat
e["duree"] += "%02d" % m heure_debut_dt = e["heure_debut"] or datetime.time(
else: 8, 00
e["duree"] = "" ) # au cas ou pas d'heure (note externe?)
if heure_debut and (not heure_fin or heure_fin == heure_debut): heure_fin_dt = e["heure_fin"] or datetime.time(8, 00)
e["descrheure"] = " à " + heure_debut e["heure_debut"] = ndb.TimefromISO8601(e["heure_debut"])
elif heure_debut and heure_fin: e["heure_fin"] = ndb.TimefromISO8601(e["heure_fin"])
e["descrheure"] = " de %s à %s" % (heure_debut, heure_fin) e["jouriso"] = ndb.DateDMYtoISO(e["jour"])
else: heure_debut, heure_fin = e["heure_debut"], e["heure_fin"]
e["descrheure"] = "" d = ndb.TimeDuration(heure_debut, heure_fin)
# matin, apresmidi: utile pour se referer aux absences: if d is not None:
m = d % 60
if e["jour"] and heure_debut_dt < datetime.time(12, 00): e["duree"] = "%dh" % (d / 60)
e["matin"] = 1 if m != 0:
else: e["duree"] += "%02d" % m
e["matin"] = 0 else:
if e["jour"] and heure_fin_dt > datetime.time(12, 00): e["duree"] = ""
e["apresmidi"] = 1 if heure_debut and (not heure_fin or heure_fin == heure_debut):
else: e["descrheure"] = " à " + heure_debut
e["apresmidi"] = 0 elif heure_debut and heure_fin:
return e e["descrheure"] = " de %s à %s" % (heure_debut, heure_fin)
else:
e["descrheure"] = ""
def check_evaluation_args(args): # matin, apresmidi: utile pour se referer aux absences:
"Check coefficient, dates and duration, raises exception if invalid"
moduleimpl_id = args["moduleimpl_id"] if e["jour"] and heure_debut_dt < datetime.time(12, 00):
# check bareme e["matin"] = 1
note_max = args.get("note_max", None) else:
if note_max is None: e["matin"] = 0
raise ScoValueError("missing note_max") if e["jour"] and heure_fin_dt > datetime.time(12, 00):
try: e["apresmidi"] = 1
note_max = float(note_max) else:
except ValueError: e["apresmidi"] = 0
raise ScoValueError("Invalid note_max value") return e
if note_max < 0:
raise ScoValueError("Invalid note_max value (must be positive or null)")
# check coefficient def check_evaluation_args(args):
coef = args.get("coefficient", None) "Check coefficient, dates and duration, raises exception if invalid"
if coef is None: moduleimpl_id = args["moduleimpl_id"]
raise ScoValueError("missing coefficient") # check bareme
try: note_max = args.get("note_max", None)
coef = float(coef) if note_max is None:
except ValueError: raise ScoValueError("missing note_max")
raise ScoValueError("Invalid coefficient value") try:
if coef < 0: note_max = float(note_max)
raise ScoValueError("Invalid coefficient value (must be positive or null)") except ValueError:
# check date raise ScoValueError("Invalid note_max value")
jour = args.get("jour", None) if note_max < 0:
args["jour"] = jour raise ScoValueError("Invalid note_max value (must be positive or null)")
if jour: # check coefficient
modimpl = ModuleImpl.query.get(moduleimpl_id) coef = args.get("coefficient", None)
formsemestre = modimpl.formsemestre if coef is None:
y, m, d = [int(x) for x in ndb.DateDMYtoISO(jour).split("-")] raise ScoValueError("missing coefficient")
jour = datetime.date(y, m, d) try:
if (jour > formsemestre.date_fin) or (jour < formsemestre.date_debut): coef = float(coef)
raise ScoValueError( except ValueError:
"La date de l'évaluation (%s/%s/%s) n'est pas dans le semestre !" raise ScoValueError("Invalid coefficient value")
% (d, m, y), if coef < 0:
dest_url="javascript:history.back();", raise ScoValueError("Invalid coefficient value (must be positive or null)")
) # check date
heure_debut = args.get("heure_debut", None) jour = args.get("jour", None)
args["heure_debut"] = heure_debut args["jour"] = jour
heure_fin = args.get("heure_fin", None) if jour:
args["heure_fin"] = heure_fin modimpl = ModuleImpl.query.get(moduleimpl_id)
if jour and ((not heure_debut) or (not heure_fin)): formsemestre = modimpl.formsemestre
raise ScoValueError("Les heures doivent être précisées") y, m, d = [int(x) for x in ndb.DateDMYtoISO(jour).split("-")]
d = ndb.TimeDuration(heure_debut, heure_fin) jour = datetime.date(y, m, d)
if d and ((d < 0) or (d > 60 * 12)): if (jour > formsemestre.date_fin) or (jour < formsemestre.date_debut):
raise ScoValueError("Heures de l'évaluation incohérentes !") raise ScoValueError(
"La date de l'évaluation (%s/%s/%s) n'est pas dans le semestre !"
% (d, m, y),
dest_url="javascript:history.back();",
)
heure_debut = args.get("heure_debut", None)
args["heure_debut"] = heure_debut
heure_fin = args.get("heure_fin", None)
args["heure_fin"] = heure_fin
if jour and ((not heure_debut) or (not heure_fin)):
raise ScoValueError("Les heures doivent être précisées")
d = ndb.TimeDuration(heure_debut, heure_fin)
if d and ((d < 0) or (d > 60 * 12)):
raise ScoValueError("Heures de l'évaluation incohérentes !")

File diff suppressed because it is too large Load Diff

View File

@ -1862,6 +1862,14 @@ a.mievr_evalnodate:hover {
text-decoration: underline; text-decoration: underline;
} }
span.eval_warning_coef {
color: red;
margin: 2px;
padding-left: 3px;
padding-right: 3px;
background-color: rgb(255, 225, 0);
}
span.evalindex_cont { span.evalindex_cont {
float: right; float: right;
} }