2021-11-13 08:25:51 +01:00
|
|
|
# -*- coding: UTF-8 -*
|
|
|
|
|
|
|
|
"""ScoDoc models: evaluations
|
|
|
|
"""
|
2022-02-10 21:55:06 +01:00
|
|
|
import datetime
|
2023-04-03 17:46:31 +02:00
|
|
|
from operator import attrgetter
|
2021-11-13 08:25:51 +01:00
|
|
|
|
2024-02-04 12:00:26 +01:00
|
|
|
from flask import abort, g, url_for
|
2023-08-22 17:02:00 +02:00
|
|
|
from flask_login import current_user
|
|
|
|
import sqlalchemy as sa
|
|
|
|
|
2023-08-26 16:34:56 +02:00
|
|
|
from app import db, log
|
2024-02-25 16:58:59 +01:00
|
|
|
from app import models
|
2022-10-05 23:48:54 +02:00
|
|
|
from app.models.etudiants import Identite
|
2023-08-22 17:02:00 +02:00
|
|
|
from app.models.events import ScolarNews
|
2022-10-05 23:48:54 +02:00
|
|
|
from app.models.notes import NotesNotes
|
2021-11-13 08:25:51 +01:00
|
|
|
|
2023-08-22 17:02:00 +02:00
|
|
|
from app.scodoc import sco_cache
|
|
|
|
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
|
|
|
import app.scodoc.sco_utils as scu
|
2023-08-25 17:58:57 +02:00
|
|
|
from app.scodoc.sco_xml import quote_xml_attr
|
2021-11-13 08:25:51 +01:00
|
|
|
|
2023-08-22 17:02:00 +02:00
|
|
|
MAX_EVALUATION_DURATION = datetime.timedelta(days=365)
|
|
|
|
NOON = datetime.time(12, 00)
|
2022-12-15 17:09:35 +01:00
|
|
|
DEFAULT_EVALUATION_TIME = datetime.time(8, 0)
|
|
|
|
|
2021-11-13 08:25:51 +01:00
|
|
|
|
2024-02-25 16:58:59 +01:00
|
|
|
class Evaluation(models.ScoDocModel):
|
2021-11-13 08:25:51 +01:00
|
|
|
"""Evaluation (contrôle, examen, ...)"""
|
|
|
|
|
|
|
|
__tablename__ = "notes_evaluation"
|
|
|
|
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
|
|
evaluation_id = db.synonym("id")
|
|
|
|
moduleimpl_id = db.Column(
|
|
|
|
db.Integer, db.ForeignKey("notes_moduleimpl.id"), index=True
|
|
|
|
)
|
2023-08-22 17:02:00 +02:00
|
|
|
date_debut = db.Column(db.DateTime(timezone=True), nullable=True)
|
|
|
|
date_fin = db.Column(db.DateTime(timezone=True), nullable=True)
|
2024-02-25 16:58:59 +01:00
|
|
|
description = db.Column(db.Text, nullable=False)
|
|
|
|
note_max = db.Column(db.Float, nullable=False)
|
|
|
|
coefficient = db.Column(db.Float, nullable=False)
|
2021-11-13 08:25:51 +01:00
|
|
|
visibulletin = db.Column(
|
|
|
|
db.Boolean, nullable=False, default=True, server_default="true"
|
|
|
|
)
|
2023-08-25 17:58:57 +02:00
|
|
|
"visible sur les bulletins version intermédiaire"
|
2021-11-13 08:25:51 +01:00
|
|
|
publish_incomplete = db.Column(
|
|
|
|
db.Boolean, nullable=False, default=False, server_default="false"
|
|
|
|
)
|
2024-02-25 16:58:59 +01:00
|
|
|
"prise en compte immédiate"
|
2021-11-13 08:25:51 +01:00
|
|
|
evaluation_type = db.Column(
|
|
|
|
db.Integer, nullable=False, default=0, server_default="0"
|
|
|
|
)
|
2024-02-25 16:58:59 +01:00
|
|
|
"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)
|
2021-11-13 08:25:51 +01:00
|
|
|
# ordre de presentation (par défaut, le plus petit numero
|
|
|
|
# est la plus ancienne eval):
|
2023-04-03 17:46:31 +02:00
|
|
|
numero = db.Column(db.Integer, nullable=False, default=0)
|
2021-11-13 08:25:51 +01:00
|
|
|
ues = db.relationship("UniteEns", secondary="evaluation_ue_poids", viewonly=True)
|
|
|
|
|
2024-02-24 16:49:41 +01:00
|
|
|
EVALUATION_NORMALE = 0 # valeurs stockées en base, ne pas changer !
|
|
|
|
EVALUATION_RATTRAPAGE = 1
|
|
|
|
EVALUATION_SESSION2 = 2
|
|
|
|
EVALUATION_BONUS = 3
|
|
|
|
VALID_EVALUATION_TYPES = {
|
|
|
|
EVALUATION_NORMALE,
|
|
|
|
EVALUATION_RATTRAPAGE,
|
|
|
|
EVALUATION_SESSION2,
|
|
|
|
EVALUATION_BONUS,
|
|
|
|
}
|
|
|
|
|
2024-05-20 10:01:39 +02:00
|
|
|
def type_abbrev(self) -> str:
|
|
|
|
"Le nom abrégé du type de cette éval."
|
|
|
|
return {
|
|
|
|
self.EVALUATION_NORMALE: "std",
|
|
|
|
self.EVALUATION_RATTRAPAGE: "rattrapage",
|
|
|
|
self.EVALUATION_SESSION2: "session 2",
|
|
|
|
self.EVALUATION_BONUS: "bonus",
|
|
|
|
}.get(self.evaluation_type, "?")
|
|
|
|
|
2021-11-20 16:35:09 +01:00
|
|
|
def __repr__(self):
|
2022-10-01 18:55:32 +02:00
|
|
|
return f"""<Evaluation {self.id} {
|
2023-08-22 17:02:00 +02:00
|
|
|
self.date_debut.isoformat() if self.date_debut else ''} "{
|
2022-10-01 18:55:32 +02:00
|
|
|
self.description[:16] if self.description else ''}">"""
|
2021-11-20 16:35:09 +01:00
|
|
|
|
2023-08-22 17:02:00 +02:00
|
|
|
@classmethod
|
|
|
|
def create(
|
|
|
|
cls,
|
2023-12-10 20:59:32 +01:00
|
|
|
moduleimpl: "ModuleImpl" = None,
|
2023-08-25 17:58:57 +02:00
|
|
|
date_debut: datetime.datetime = None,
|
|
|
|
date_fin: datetime.datetime = None,
|
2023-08-22 17:02:00 +02:00
|
|
|
description=None,
|
|
|
|
note_max=None,
|
2024-02-25 16:58:59 +01:00
|
|
|
blocked_until=None,
|
2023-08-22 17:02:00 +02:00
|
|
|
coefficient=None,
|
|
|
|
visibulletin=None,
|
|
|
|
publish_incomplete=None,
|
|
|
|
evaluation_type=None,
|
|
|
|
numero=None,
|
|
|
|
**kw, # ceci pour absorber les éventuel arguments excedentaires
|
2024-02-26 17:20:36 +01:00
|
|
|
) -> "Evaluation":
|
2023-08-26 16:34:56 +02:00
|
|
|
"""Create an evaluation. Check permission and all arguments.
|
|
|
|
Ne crée pas les poids vers les UEs.
|
2024-02-14 21:45:58 +01:00
|
|
|
Add to session, do not commit.
|
2023-08-26 16:34:56 +02:00
|
|
|
"""
|
2023-08-22 17:02:00 +02:00
|
|
|
if not moduleimpl.can_edit_evaluation(current_user):
|
|
|
|
raise AccessDenied(
|
|
|
|
f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
|
|
|
|
)
|
|
|
|
args = locals()
|
|
|
|
del args["cls"]
|
|
|
|
del args["kw"]
|
2024-02-26 17:20:36 +01:00
|
|
|
check_and_convert_evaluation_args(args, moduleimpl)
|
2023-08-22 17:02:00 +02:00
|
|
|
# Check numeros
|
|
|
|
Evaluation.moduleimpl_evaluation_renumber(moduleimpl, only_if_unumbered=True)
|
|
|
|
if not "numero" in args or args["numero"] is None:
|
|
|
|
args["numero"] = cls.get_new_numero(moduleimpl, args["date_debut"])
|
|
|
|
#
|
|
|
|
evaluation = Evaluation(**args)
|
2024-02-14 21:45:58 +01:00
|
|
|
db.session.add(evaluation)
|
|
|
|
db.session.flush()
|
2023-08-22 17:02:00 +02:00
|
|
|
sco_cache.invalidate_formsemestre(formsemestre_id=moduleimpl.formsemestre_id)
|
|
|
|
url = url_for(
|
|
|
|
"notes.moduleimpl_status",
|
|
|
|
scodoc_dept=g.scodoc_dept,
|
|
|
|
moduleimpl_id=moduleimpl.id,
|
|
|
|
)
|
2023-08-26 16:34:56 +02:00
|
|
|
log(f"created evaluation in {moduleimpl.module.titre_str()}")
|
2023-08-22 17:02:00 +02:00
|
|
|
ScolarNews.add(
|
|
|
|
typ=ScolarNews.NEWS_NOTE,
|
|
|
|
obj=moduleimpl.id,
|
|
|
|
text=f"""Création d'une évaluation dans <a href="{url}">{
|
2023-08-26 16:34:56 +02:00
|
|
|
moduleimpl.module.titre_str()}</a>""",
|
2023-08-22 17:02:00 +02:00
|
|
|
url=url,
|
|
|
|
)
|
|
|
|
return evaluation
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def get_new_numero(
|
2023-12-10 20:59:32 +01:00
|
|
|
cls, moduleimpl: "ModuleImpl", date_debut: datetime.datetime
|
2023-08-22 17:02:00 +02:00
|
|
|
) -> int:
|
|
|
|
"""Get a new numero for an evaluation in this moduleimpl
|
|
|
|
If necessary, renumber existing evals to make room for a new one.
|
|
|
|
"""
|
|
|
|
n = None
|
|
|
|
# Détermine le numero grâce à la date
|
|
|
|
# Liste des eval existantes triées par date, la plus ancienne en tete
|
|
|
|
evaluations = moduleimpl.evaluations.order_by(Evaluation.date_debut).all()
|
|
|
|
if date_debut is not None:
|
|
|
|
next_eval = None
|
|
|
|
t = date_debut
|
|
|
|
for e in evaluations:
|
2023-08-25 17:58:57 +02:00
|
|
|
if e.date_debut and e.date_debut > t:
|
2023-08-22 17:02:00 +02:00
|
|
|
next_eval = e
|
|
|
|
break
|
|
|
|
if next_eval:
|
|
|
|
n = _moduleimpl_evaluation_insert_before(evaluations, next_eval)
|
|
|
|
else:
|
|
|
|
n = None # à placer en fin
|
|
|
|
if n is None: # pas de date ou en fin:
|
|
|
|
if evaluations:
|
|
|
|
n = evaluations[-1].numero + 1
|
|
|
|
else:
|
|
|
|
n = 0 # the only one
|
|
|
|
return n
|
|
|
|
|
2023-08-30 09:22:51 +02:00
|
|
|
def delete(self):
|
|
|
|
"delete evaluation (commit) (check permission)"
|
|
|
|
from app.scodoc import sco_evaluation_db
|
|
|
|
|
2023-12-10 20:59:32 +01:00
|
|
|
modimpl: "ModuleImpl" = self.moduleimpl
|
2023-08-30 09:22:51 +02:00
|
|
|
if not modimpl.can_edit_evaluation(current_user):
|
|
|
|
raise AccessDenied(
|
|
|
|
f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
|
|
|
|
)
|
|
|
|
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(
|
|
|
|
self.id
|
|
|
|
) # { etudid : value }
|
|
|
|
notes = [x["value"] for x in notes_db.values()]
|
|
|
|
if notes:
|
|
|
|
raise ScoValueError(
|
|
|
|
"Impossible de supprimer cette évaluation: il reste des notes"
|
|
|
|
)
|
|
|
|
log(f"deleting evaluation {self}")
|
|
|
|
db.session.delete(self)
|
|
|
|
db.session.commit()
|
|
|
|
|
|
|
|
# inval cache pour ce semestre
|
|
|
|
sco_cache.invalidate_formsemestre(formsemestre_id=modimpl.formsemestre_id)
|
|
|
|
# news
|
|
|
|
url = url_for(
|
|
|
|
"notes.moduleimpl_status",
|
|
|
|
scodoc_dept=g.scodoc_dept,
|
|
|
|
moduleimpl_id=modimpl.id,
|
|
|
|
)
|
|
|
|
ScolarNews.add(
|
|
|
|
typ=ScolarNews.NEWS_NOTE,
|
|
|
|
obj=modimpl.id,
|
|
|
|
text=f"""Suppression d'une évaluation dans <a href="{
|
|
|
|
url
|
|
|
|
}">{modimpl.module.titre}</a>""",
|
|
|
|
url=url,
|
|
|
|
)
|
|
|
|
|
2022-05-19 04:15:26 +02:00
|
|
|
def to_dict(self) -> dict:
|
2022-11-01 11:19:28 +01:00
|
|
|
"Représentation dict (riche, compat ScoDoc 7)"
|
2023-08-25 17:58:57 +02:00
|
|
|
e_dict = dict(self.__dict__)
|
|
|
|
e_dict.pop("_sa_instance_state", None)
|
2021-11-13 08:25:51 +01:00
|
|
|
# ScoDoc7 output_formators
|
2023-08-25 17:58:57 +02:00
|
|
|
e_dict["evaluation_id"] = self.id
|
|
|
|
e_dict["date_debut"] = self.date_debut.isoformat() if self.date_debut else None
|
2024-01-22 09:57:41 +01:00
|
|
|
e_dict["date_fin"] = self.date_fin.isoformat() if self.date_fin else None
|
2023-08-25 17:58:57 +02:00
|
|
|
e_dict["numero"] = self.numero or 0
|
|
|
|
e_dict["poids"] = self.get_ue_poids_dict() # { ue_id : poids }
|
2023-08-22 17:02:00 +02:00
|
|
|
|
|
|
|
# Deprecated
|
2024-04-02 23:37:23 +02:00
|
|
|
e_dict["jour"] = (
|
|
|
|
self.date_debut.strftime(scu.DATE_FMT) if self.date_debut else ""
|
|
|
|
)
|
2023-08-22 17:02:00 +02:00
|
|
|
|
2023-08-25 17:58:57 +02:00
|
|
|
return evaluation_enrich_dict(self, e_dict)
|
2021-11-13 08:25:51 +01:00
|
|
|
|
2022-11-01 11:19:28 +01:00
|
|
|
def to_dict_api(self) -> dict:
|
|
|
|
"Représentation dict pour API JSON"
|
|
|
|
return {
|
2024-02-25 16:58:59 +01:00
|
|
|
"blocked": self.is_blocked(),
|
|
|
|
"blocked_until": (
|
|
|
|
self.blocked_until.isoformat() if self.blocked_until else ""
|
|
|
|
),
|
2022-11-01 11:19:28 +01:00
|
|
|
"coefficient": self.coefficient,
|
2023-08-25 17:58:57 +02:00
|
|
|
"date_debut": self.date_debut.isoformat() if self.date_debut else "",
|
|
|
|
"date_fin": self.date_fin.isoformat() if self.date_fin else "",
|
2022-11-01 11:19:28 +01:00
|
|
|
"description": self.description,
|
|
|
|
"evaluation_type": self.evaluation_type,
|
|
|
|
"id": self.id,
|
|
|
|
"moduleimpl_id": self.moduleimpl_id,
|
|
|
|
"note_max": self.note_max,
|
|
|
|
"numero": self.numero,
|
|
|
|
"poids": self.get_ue_poids_dict(),
|
|
|
|
"publish_incomplete": self.publish_incomplete,
|
2023-08-25 17:58:57 +02:00
|
|
|
"visibulletin": self.visibulletin,
|
|
|
|
# Deprecated (supprimer avant #sco9.7)
|
|
|
|
"date": self.date_debut.date().isoformat() if self.date_debut else "",
|
2024-02-14 21:45:58 +01:00
|
|
|
"heure_debut": (
|
|
|
|
self.date_debut.time().isoformat() if self.date_debut else ""
|
|
|
|
),
|
2023-08-25 17:58:57 +02:00
|
|
|
"heure_fin": self.date_fin.time().isoformat() if self.date_fin else "",
|
2022-11-01 11:19:28 +01:00
|
|
|
}
|
|
|
|
|
2023-08-25 17:58:57 +02:00
|
|
|
def to_dict_bul(self) -> dict:
|
|
|
|
"dict pour les bulletins json"
|
|
|
|
# c'est la version API avec quelques champs legacy en plus
|
|
|
|
e_dict = self.to_dict_api()
|
|
|
|
# Pour les bulletins (json ou xml), quote toujours la description
|
|
|
|
e_dict["description"] = quote_xml_attr(self.description or "")
|
|
|
|
# deprecated fields:
|
|
|
|
e_dict["evaluation_id"] = self.id
|
|
|
|
e_dict["jour"] = e_dict["date_debut"] # chaine iso
|
|
|
|
e_dict["heure_debut"] = (
|
|
|
|
self.date_debut.time().isoformat() if self.date_debut else ""
|
|
|
|
)
|
|
|
|
e_dict["heure_fin"] = self.date_fin.time().isoformat() if self.date_fin else ""
|
|
|
|
|
|
|
|
return e_dict
|
|
|
|
|
2024-02-04 12:00:26 +01:00
|
|
|
@classmethod
|
|
|
|
def get_evaluation(
|
2024-07-14 22:20:37 +02:00
|
|
|
cls, evaluation_id: int | str, dept_id: int = None, accept_none=False
|
2024-02-04 12:00:26 +01:00
|
|
|
) -> "Evaluation":
|
2024-07-14 22:20:37 +02:00
|
|
|
"""Evaluation ou 404, cherche uniquement dans le département spécifié ou le courant.
|
|
|
|
Si accept_none, return None si l'id est invalide ou n'existe pas.
|
|
|
|
"""
|
|
|
|
from app.models import FormSemestre
|
2024-02-04 12:00:26 +01:00
|
|
|
|
|
|
|
if not isinstance(evaluation_id, int):
|
|
|
|
try:
|
|
|
|
evaluation_id = int(evaluation_id)
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
abort(404, "evaluation_id invalide")
|
|
|
|
if g.scodoc_dept:
|
|
|
|
dept_id = dept_id if dept_id is not None else g.scodoc_dept_id
|
|
|
|
query = cls.query.filter_by(id=evaluation_id)
|
|
|
|
if dept_id is not None:
|
|
|
|
query = query.join(ModuleImpl).join(FormSemestre).filter_by(dept_id=dept_id)
|
2024-07-14 22:20:37 +02:00
|
|
|
if accept_none:
|
|
|
|
return query.first()
|
2024-02-04 12:00:26 +01:00
|
|
|
return query.first_or_404()
|
|
|
|
|
2023-08-22 17:02:00 +02:00
|
|
|
@classmethod
|
|
|
|
def get_max_numero(cls, moduleimpl_id: int) -> int:
|
|
|
|
"""Return max numero among evaluations in this
|
|
|
|
moduleimpl (0 if None)
|
|
|
|
"""
|
|
|
|
max_num = (
|
|
|
|
db.session.query(sa.sql.functions.max(Evaluation.numero))
|
|
|
|
.filter_by(moduleimpl_id=moduleimpl_id)
|
|
|
|
.first()[0]
|
|
|
|
)
|
|
|
|
return max_num or 0
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def moduleimpl_evaluation_renumber(
|
2023-12-10 20:59:32 +01:00
|
|
|
cls, moduleimpl: "ModuleImpl", only_if_unumbered=False
|
2023-08-22 17:02:00 +02:00
|
|
|
):
|
|
|
|
"""Renumber evaluations in this moduleimpl, according to their date. (numero=0: oldest one)
|
|
|
|
Needed because previous versions of ScoDoc did not have eval numeros
|
|
|
|
Note: existing numeros are ignored
|
|
|
|
"""
|
|
|
|
# Liste des eval existantes triées par date, la plus ancienne en tete
|
|
|
|
evaluations = moduleimpl.evaluations.order_by(
|
|
|
|
Evaluation.date_debut, Evaluation.numero
|
|
|
|
).all()
|
2024-02-04 12:00:26 +01:00
|
|
|
numeros_distincts = {e.numero for e in evaluations if e.numero is not None}
|
|
|
|
# pas de None, pas de dupliqués
|
|
|
|
all_numbered = len(numeros_distincts) == len(evaluations)
|
2023-08-22 17:02:00 +02:00
|
|
|
if all_numbered and only_if_unumbered:
|
|
|
|
return # all ok
|
|
|
|
|
|
|
|
# Reset all numeros:
|
|
|
|
i = 1
|
|
|
|
for e in evaluations:
|
|
|
|
e.numero = i
|
|
|
|
db.session.add(e)
|
|
|
|
i += 1
|
|
|
|
db.session.commit()
|
2023-12-12 19:12:46 +01:00
|
|
|
sco_cache.invalidate_formsemestre(moduleimpl.formsemestre_id)
|
2023-08-22 17:02:00 +02:00
|
|
|
|
2022-10-01 18:55:32 +02:00
|
|
|
def descr_heure(self) -> str:
|
2023-08-25 17:58:57 +02:00
|
|
|
"Description de la plage horaire pour affichages ('de 13h00 à 14h00')"
|
|
|
|
if self.date_debut and (not self.date_fin or self.date_fin == self.date_debut):
|
2024-04-02 23:37:23 +02:00
|
|
|
return f"""à {self.date_debut.strftime(scu.TIME_FMT)}"""
|
2023-08-25 17:58:57 +02:00
|
|
|
elif self.date_debut and self.date_fin:
|
2024-04-02 23:37:23 +02:00
|
|
|
return f"""de {self.date_debut.strftime(scu.TIME_FMT)
|
|
|
|
} à {self.date_fin.strftime(scu.TIME_FMT)}"""
|
2022-10-01 18:55:32 +02:00
|
|
|
else:
|
|
|
|
return ""
|
|
|
|
|
2022-12-15 17:09:35 +01:00
|
|
|
def descr_duree(self) -> str:
|
2023-08-25 17:58:57 +02:00
|
|
|
"Description de la durée pour affichages ('3h' ou '2h30')"
|
|
|
|
if self.date_debut is None or self.date_fin is None:
|
2022-12-15 17:09:35 +01:00
|
|
|
return ""
|
2023-08-25 17:58:57 +02:00
|
|
|
minutes = (self.date_fin - self.date_debut).seconds // 60
|
|
|
|
duree = f"{minutes // 60}h"
|
|
|
|
minutes = minutes % 60
|
|
|
|
if minutes != 0:
|
|
|
|
duree += f"{minutes:02d}"
|
2022-12-15 17:09:35 +01:00
|
|
|
return duree
|
|
|
|
|
2023-08-25 17:58:57 +02:00
|
|
|
def descr_date(self) -> str:
|
|
|
|
"""Description de la date pour affichages
|
|
|
|
'sans date'
|
|
|
|
'le 21/9/2021 à 13h'
|
|
|
|
'le 21/9/2021 de 13h à 14h30'
|
|
|
|
'du 21/9/2021 à 13h30 au 23/9/2021 à 15h'
|
|
|
|
"""
|
|
|
|
if self.date_debut is None:
|
|
|
|
return "sans date"
|
|
|
|
|
|
|
|
def _h(dt: datetime.datetime) -> str:
|
|
|
|
if dt.minute:
|
2024-04-02 23:37:23 +02:00
|
|
|
return dt.strftime(scu.TIME_FMT)
|
2023-08-25 17:58:57 +02:00
|
|
|
return f"{dt.hour}h"
|
|
|
|
|
|
|
|
if self.date_fin is None:
|
2023-08-26 16:34:56 +02:00
|
|
|
return f"le {self.date_debut.strftime('%d/%m/%Y')} à {_h(self.date_debut)}"
|
2023-08-25 17:58:57 +02:00
|
|
|
if self.date_debut.date() == self.date_fin.date(): # même jour
|
|
|
|
if self.date_debut.time() == self.date_fin.time():
|
2024-06-28 19:03:20 +02:00
|
|
|
if self.date_fin.time() == datetime.time(0, 0):
|
|
|
|
return f"le {self.date_debut.strftime('%d/%m/%Y')}" # sans heure
|
2023-08-26 16:34:56 +02:00
|
|
|
return (
|
|
|
|
f"le {self.date_debut.strftime('%d/%m/%Y')} à {_h(self.date_debut)}"
|
|
|
|
)
|
2023-08-25 17:58:57 +02:00
|
|
|
return f"""le {self.date_debut.strftime('%d/%m/%Y')} de {
|
2023-08-26 16:34:56 +02:00
|
|
|
_h(self.date_debut)} à {_h(self.date_fin)}"""
|
2023-08-25 17:58:57 +02:00
|
|
|
# évaluation sur plus d'une journée
|
|
|
|
return f"""du {self.date_debut.strftime('%d/%m/%Y')} à {
|
2023-08-26 16:34:56 +02:00
|
|
|
_h(self.date_debut)} au {self.date_fin.strftime('%d/%m/%Y')} à {_h(self.date_fin)}"""
|
2023-08-25 17:58:57 +02:00
|
|
|
|
|
|
|
def heure_debut(self) -> str:
|
|
|
|
"""L'heure de début (sans la date), en ISO.
|
|
|
|
Chaine vide si non renseignée."""
|
|
|
|
return self.date_debut.time().isoformat("minutes") if self.date_debut else ""
|
|
|
|
|
|
|
|
def heure_fin(self) -> str:
|
|
|
|
"""L'heure de fin (sans la date), en ISO.
|
|
|
|
Chaine vide si non renseignée."""
|
|
|
|
return self.date_fin.time().isoformat("minutes") if self.date_fin else ""
|
|
|
|
|
2023-05-29 16:04:41 +02:00
|
|
|
def is_matin(self) -> bool:
|
2023-08-22 17:02:00 +02:00
|
|
|
"Evaluation commençant le matin (faux si pas de date)"
|
|
|
|
if not self.date_debut:
|
|
|
|
return False
|
|
|
|
return self.date_debut.time() < NOON
|
2023-05-29 16:04:41 +02:00
|
|
|
|
|
|
|
def is_apresmidi(self) -> bool:
|
2023-08-22 17:02:00 +02:00
|
|
|
"Evaluation commençant l'après midi (faux si pas de date)"
|
|
|
|
if not self.date_debut:
|
|
|
|
return False
|
|
|
|
return self.date_debut.time() >= NOON
|
2023-05-29 16:04:41 +02:00
|
|
|
|
2024-02-25 16:58:59 +01:00
|
|
|
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
|
|
|
|
|
2022-10-05 10:31:25 +02:00
|
|
|
def set_default_poids(self) -> bool:
|
2023-08-22 17:02:00 +02:00
|
|
|
"""Initialize les poids vers les UE à leurs valeurs par défaut
|
2022-10-05 10:31:25 +02:00
|
|
|
C'est à dire à 1 si le coef. module/UE est non nul, 0 sinon.
|
|
|
|
Les poids existants ne sont pas modifiés.
|
|
|
|
Return True if (uncommited) modification, False otherwise.
|
|
|
|
"""
|
|
|
|
ue_coef_dict = self.moduleimpl.module.get_ue_coef_dict()
|
2023-04-03 17:46:31 +02:00
|
|
|
sem_ues = self.moduleimpl.formsemestre.get_ues(with_sport=False)
|
2022-10-05 10:31:25 +02:00
|
|
|
modified = False
|
|
|
|
for ue in sem_ues:
|
|
|
|
existing_poids = EvaluationUEPoids.query.filter_by(
|
|
|
|
ue=ue, evaluation=self
|
|
|
|
).first()
|
|
|
|
if existing_poids is None:
|
|
|
|
coef_ue = ue_coef_dict.get(ue.id, 0.0) or 0.0
|
|
|
|
if coef_ue > 0:
|
|
|
|
poids = 1.0 # par défaut au départ
|
|
|
|
else:
|
|
|
|
poids = 0.0
|
|
|
|
self.set_ue_poids(ue, poids)
|
|
|
|
modified = True
|
|
|
|
return modified
|
|
|
|
|
2021-12-19 11:08:03 +01:00
|
|
|
def set_ue_poids(self, ue, poids: float) -> None:
|
2024-05-20 10:01:39 +02:00
|
|
|
"""Set poids évaluation vers cette UE. Commit."""
|
2021-11-13 08:25:51 +01:00
|
|
|
self.update_ue_poids_dict({ue.id: poids})
|
|
|
|
|
2021-12-19 11:08:03 +01:00
|
|
|
def set_ue_poids_dict(self, ue_poids_dict: dict) -> None:
|
2021-11-13 08:25:51 +01:00
|
|
|
"""set poids vers les UE (remplace existants)
|
|
|
|
ue_poids_dict = { ue_id : poids }
|
2024-05-20 10:01:39 +02:00
|
|
|
Commit session.
|
2021-11-13 08:25:51 +01:00
|
|
|
"""
|
2023-12-10 20:59:32 +01:00
|
|
|
from app.models.ues import UniteEns
|
|
|
|
|
2021-11-13 08:25:51 +01:00
|
|
|
L = []
|
|
|
|
for ue_id, poids in ue_poids_dict.items():
|
2023-07-11 06:57:38 +02:00
|
|
|
ue = db.session.get(UniteEns, ue_id)
|
2023-08-26 16:34:56 +02:00
|
|
|
if ue is None:
|
|
|
|
raise ScoValueError("poids vers une UE inexistante")
|
2023-07-11 06:57:38 +02:00
|
|
|
ue_poids = EvaluationUEPoids(evaluation=self, ue=ue, poids=poids)
|
|
|
|
db.session.add(ue_poids)
|
2024-05-20 10:01:39 +02:00
|
|
|
L.append(ue_poids)
|
|
|
|
|
2022-10-03 09:04:04 +02:00
|
|
|
self.ue_poids = L # backref # pylint:disable=attribute-defined-outside-init
|
2024-05-20 10:01:39 +02:00
|
|
|
|
|
|
|
db.session.commit()
|
2021-11-29 22:18:37 +01:00
|
|
|
self.moduleimpl.invalidate_evaluations_poids() # inval cache
|
2021-11-13 08:25:51 +01:00
|
|
|
|
2021-12-19 11:08:03 +01:00
|
|
|
def update_ue_poids_dict(self, ue_poids_dict: dict) -> None:
|
2021-11-13 08:25:51 +01:00
|
|
|
"""update poids vers UE (ajoute aux existants)"""
|
|
|
|
current = self.get_ue_poids_dict()
|
|
|
|
current.update(ue_poids_dict)
|
|
|
|
self.set_ue_poids_dict(current)
|
|
|
|
|
2022-10-03 09:04:04 +02:00
|
|
|
def get_ue_poids_dict(self, sort=False) -> dict:
|
|
|
|
"""returns { ue_id : poids }
|
|
|
|
Si sort, trie par UE
|
|
|
|
"""
|
|
|
|
if sort:
|
|
|
|
return {
|
|
|
|
p.ue.id: p.poids
|
|
|
|
for p in sorted(
|
2023-04-12 12:47:43 +02:00
|
|
|
self.ue_poids, key=attrgetter("ue.numero", "ue.acronyme")
|
2022-10-03 09:04:04 +02:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2021-11-13 08:25:51 +01:00
|
|
|
return {p.ue.id: p.poids for p in self.ue_poids}
|
|
|
|
|
2021-12-19 11:08:03 +01:00
|
|
|
def get_ue_poids_str(self) -> str:
|
|
|
|
"""string describing poids, for excel cells and pdfs
|
2024-01-25 20:54:12 +01:00
|
|
|
Note: les poids nuls ou non initialisés (poids par défaut),
|
|
|
|
ne sont pas affichés.
|
2021-12-19 11:08:03 +01:00
|
|
|
"""
|
2022-01-20 13:00:25 +01:00
|
|
|
# restreint aux UE du semestre dans lequel est cette évaluation
|
|
|
|
# au cas où le module ait changé de semestre et qu'il reste des poids
|
|
|
|
evaluation_semestre_idx = self.moduleimpl.module.semestre_id
|
|
|
|
return ", ".join(
|
|
|
|
[
|
|
|
|
f"{p.ue.acronyme}: {p.poids}"
|
2022-09-06 23:50:56 +02:00
|
|
|
for p in sorted(
|
|
|
|
self.ue_poids, key=lambda p: (p.ue.numero or 0, p.ue.acronyme)
|
|
|
|
)
|
2024-01-25 20:54:12 +01:00
|
|
|
if evaluation_semestre_idx == p.ue.semestre_idx and (p.poids or 0) > 0
|
2022-01-20 13:00:25 +01:00
|
|
|
]
|
|
|
|
)
|
2021-12-19 11:08:03 +01:00
|
|
|
|
2022-10-05 23:48:54 +02:00
|
|
|
def get_etud_note(self, etud: Identite) -> NotesNotes:
|
|
|
|
"""La note de l'étudiant, ou None si pas noté.
|
|
|
|
(nb: pas de cache, lent, ne pas utiliser pour des calculs)
|
|
|
|
"""
|
|
|
|
return NotesNotes.query.filter_by(etudid=etud.id, evaluation_id=self.id).first()
|
|
|
|
|
2024-02-25 22:35:14 +01:00
|
|
|
@classmethod
|
|
|
|
def get_evaluations_blocked_for_etud(
|
|
|
|
cls, formsemestre, etud: Identite
|
|
|
|
) -> list["Evaluation"]:
|
|
|
|
"""Liste des évaluations de ce semestre avec note pour cet étudiant et date blocage
|
|
|
|
et date blocage < FOREVER.
|
|
|
|
Si non vide, une note apparaitra dans le futur pour cet étudiant: il faut
|
|
|
|
donc interdire la saisie du jury.
|
|
|
|
"""
|
|
|
|
now = datetime.datetime.now(scu.TIME_ZONE)
|
|
|
|
return (
|
|
|
|
Evaluation.query.filter(
|
|
|
|
Evaluation.blocked_until != None, # pylint: disable=C0121
|
|
|
|
Evaluation.blocked_until >= now,
|
|
|
|
)
|
|
|
|
.join(ModuleImpl)
|
|
|
|
.filter_by(formsemestre_id=formsemestre.id)
|
|
|
|
.join(ModuleImplInscription)
|
|
|
|
.filter_by(etudid=etud.id)
|
|
|
|
.join(NotesNotes)
|
|
|
|
.all()
|
|
|
|
)
|
|
|
|
|
2021-11-13 08:25:51 +01:00
|
|
|
|
|
|
|
class EvaluationUEPoids(db.Model):
|
|
|
|
"""Poids des évaluations (BUT)
|
|
|
|
association many to many
|
|
|
|
"""
|
|
|
|
|
|
|
|
evaluation_id = db.Column(
|
2021-12-13 23:44:13 +01:00
|
|
|
db.Integer,
|
|
|
|
db.ForeignKey("notes_evaluation.id", ondelete="CASCADE"),
|
|
|
|
primary_key=True,
|
|
|
|
)
|
|
|
|
ue_id = db.Column(
|
|
|
|
db.Integer,
|
|
|
|
db.ForeignKey("notes_ue.id", ondelete="CASCADE"),
|
|
|
|
primary_key=True,
|
2021-11-13 08:25:51 +01:00
|
|
|
)
|
|
|
|
poids = db.Column(
|
|
|
|
db.Float,
|
|
|
|
nullable=False,
|
|
|
|
)
|
|
|
|
evaluation = db.relationship(
|
|
|
|
Evaluation,
|
|
|
|
backref=db.backref("ue_poids", cascade="all, delete-orphan"),
|
|
|
|
)
|
|
|
|
ue = db.relationship(
|
2023-12-10 20:59:32 +01:00
|
|
|
"UniteEns",
|
2021-11-13 08:25:51 +01:00
|
|
|
backref=db.backref("evaluation_ue_poids", cascade="all, delete-orphan"),
|
|
|
|
)
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
return f"<EvaluationUEPoids {self.evaluation} {self.ue} poids={self.poids}>"
|
2022-02-10 21:55:06 +01:00
|
|
|
|
|
|
|
|
2023-08-25 17:58:57 +02:00
|
|
|
# Fonction héritée de ScoDoc7
|
|
|
|
def evaluation_enrich_dict(e: Evaluation, e_dict: dict):
|
2022-05-11 04:14:42 +02:00
|
|
|
"""add or convert some fields in an evaluation dict"""
|
2022-02-10 21:55:06 +01:00
|
|
|
# For ScoDoc7 compat
|
2024-04-02 23:37:23 +02:00
|
|
|
e_dict["heure_debut"] = e.date_debut.strftime(scu.TIME_FMT) if e.date_debut else ""
|
|
|
|
e_dict["heure_fin"] = e.date_fin.strftime(scu.TIME_FMT) if e.date_fin else ""
|
2023-08-25 17:58:57 +02:00
|
|
|
e_dict["jour_iso"] = e.date_debut.isoformat() if e.date_debut else ""
|
|
|
|
# Calcule durée en minutes
|
|
|
|
e_dict["descrheure"] = e.descr_heure()
|
|
|
|
e_dict["descrduree"] = e.descr_duree()
|
2022-02-10 21:55:06 +01:00
|
|
|
# matin, apresmidi: utile pour se referer aux absences:
|
2023-08-25 17:58:57 +02:00
|
|
|
# note août 2023: si l'évaluation s'étend sur plusieurs jours,
|
|
|
|
# cet indicateur n'a pas grand sens
|
|
|
|
if e.date_debut and e.date_debut.time() < datetime.time(12, 00):
|
|
|
|
e_dict["matin"] = 1
|
2022-02-10 21:55:06 +01:00
|
|
|
else:
|
2023-08-25 17:58:57 +02:00
|
|
|
e_dict["matin"] = 0
|
|
|
|
if e.date_fin and e.date_fin.time() > datetime.time(12, 00):
|
|
|
|
e_dict["apresmidi"] = 1
|
2022-02-10 21:55:06 +01:00
|
|
|
else:
|
2023-08-25 17:58:57 +02:00
|
|
|
e_dict["apresmidi"] = 0
|
|
|
|
return e_dict
|
2022-02-10 21:55:06 +01:00
|
|
|
|
|
|
|
|
2024-02-26 17:20:36 +01:00
|
|
|
def check_and_convert_evaluation_args(data: dict, moduleimpl: "ModuleImpl"):
|
2023-08-22 17:02:00 +02:00
|
|
|
"""Check coefficient, dates and duration, raises exception if invalid.
|
|
|
|
Convert date and time strings to date and time objects.
|
|
|
|
|
|
|
|
Set required default value for unspecified fields.
|
|
|
|
May raise ScoValueError.
|
|
|
|
"""
|
|
|
|
# --- description
|
2023-08-25 17:58:57 +02:00
|
|
|
data["description"] = data.get("description", "") or ""
|
|
|
|
if len(data["description"]) > scu.MAX_TEXT_LEN:
|
2023-08-22 17:02:00 +02:00
|
|
|
raise ScoValueError("description too large")
|
2023-08-25 17:58:57 +02:00
|
|
|
|
2023-08-22 17:02:00 +02:00
|
|
|
# --- evaluation_type
|
|
|
|
try:
|
|
|
|
data["evaluation_type"] = int(data.get("evaluation_type", 0) or 0)
|
2024-02-24 16:49:41 +01:00
|
|
|
if not data["evaluation_type"] in Evaluation.VALID_EVALUATION_TYPES:
|
2023-08-25 17:58:57 +02:00
|
|
|
raise ScoValueError("invalid evaluation_type value")
|
|
|
|
except ValueError as exc:
|
|
|
|
raise ScoValueError("invalid evaluation_type value") from exc
|
2023-08-22 17:02:00 +02:00
|
|
|
|
|
|
|
# --- note_max (bareme)
|
|
|
|
note_max = data.get("note_max", 20.0) or 20.0
|
2022-02-10 21:55:06 +01:00
|
|
|
try:
|
|
|
|
note_max = float(note_max)
|
2023-08-25 17:58:57 +02:00
|
|
|
except ValueError as exc:
|
|
|
|
raise ScoValueError("invalid note_max value") from exc
|
2022-02-10 21:55:06 +01:00
|
|
|
if note_max < 0:
|
2023-08-25 17:58:57 +02:00
|
|
|
raise ScoValueError("invalid note_max value (must be positive or null)")
|
2023-08-22 17:02:00 +02:00
|
|
|
data["note_max"] = note_max
|
|
|
|
# --- coefficient
|
2023-09-13 18:15:10 +02:00
|
|
|
coef = data.get("coefficient", None)
|
|
|
|
if coef is None:
|
|
|
|
coef = 1.0
|
2022-02-10 21:55:06 +01:00
|
|
|
try:
|
|
|
|
coef = float(coef)
|
2023-08-25 17:58:57 +02:00
|
|
|
except ValueError as exc:
|
|
|
|
raise ScoValueError("invalid coefficient value") from exc
|
2022-02-10 21:55:06 +01:00
|
|
|
if coef < 0:
|
2023-08-25 17:58:57 +02:00
|
|
|
raise ScoValueError("invalid coefficient value (must be positive or null)")
|
2023-08-22 17:02:00 +02:00
|
|
|
data["coefficient"] = coef
|
2024-02-26 17:20:36 +01:00
|
|
|
# --- date de l'évaluation dans le semestre ?
|
2023-08-25 17:58:57 +02:00
|
|
|
formsemestre = moduleimpl.formsemestre
|
|
|
|
date_debut = data.get("date_debut", None)
|
|
|
|
if date_debut:
|
|
|
|
if isinstance(date_debut, str):
|
|
|
|
data["date_debut"] = datetime.datetime.fromisoformat(date_debut)
|
|
|
|
if data["date_debut"].tzinfo is None:
|
|
|
|
data["date_debut"] = scu.TIME_ZONE.localize(data["date_debut"])
|
|
|
|
if not (
|
|
|
|
formsemestre.date_debut
|
|
|
|
<= data["date_debut"].date()
|
|
|
|
<= formsemestre.date_fin
|
|
|
|
):
|
2022-02-10 21:55:06 +01:00
|
|
|
raise ScoValueError(
|
2023-08-25 17:58:57 +02:00
|
|
|
f"""La date de début de l'évaluation ({
|
2024-04-02 23:37:23 +02:00
|
|
|
data["date_debut"].strftime(scu.DATE_FMT)
|
2023-08-25 17:58:57 +02:00
|
|
|
}) n'est pas dans le semestre !""",
|
2022-02-10 21:55:06 +01:00
|
|
|
dest_url="javascript:history.back();",
|
|
|
|
)
|
2023-08-25 17:58:57 +02:00
|
|
|
date_fin = data.get("date_fin", None)
|
|
|
|
if date_fin:
|
|
|
|
if isinstance(date_fin, str):
|
|
|
|
data["date_fin"] = datetime.datetime.fromisoformat(date_fin)
|
|
|
|
if data["date_fin"].tzinfo is None:
|
|
|
|
data["date_fin"] = scu.TIME_ZONE.localize(data["date_fin"])
|
|
|
|
if not (
|
|
|
|
formsemestre.date_debut <= data["date_fin"].date() <= formsemestre.date_fin
|
|
|
|
):
|
|
|
|
raise ScoValueError(
|
|
|
|
f"""La date de fin de l'évaluation ({
|
2024-04-02 23:37:23 +02:00
|
|
|
data["date_fin"].strftime(scu.DATE_FMT)
|
2023-08-25 17:58:57 +02:00
|
|
|
}) n'est pas dans le semestre !""",
|
|
|
|
dest_url="javascript:history.back();",
|
|
|
|
)
|
|
|
|
if date_debut and date_fin:
|
|
|
|
duration = data["date_fin"] - data["date_debut"]
|
|
|
|
if duration.total_seconds() < 0 or duration > MAX_EVALUATION_DURATION:
|
2024-01-16 12:36:20 +01:00
|
|
|
raise ScoValueError(
|
|
|
|
"Heures de l'évaluation incohérentes !",
|
|
|
|
dest_url="javascript:history.back();",
|
|
|
|
)
|
2024-02-25 16:58:59 +01:00
|
|
|
if "blocked_until" in data:
|
|
|
|
data["blocked_until"] = data["blocked_until"] or None
|
2023-08-22 17:02:00 +02:00
|
|
|
|
|
|
|
|
|
|
|
def heure_to_time(heure: str) -> datetime.time:
|
|
|
|
"Convert external heure ('10h22' or '10:22') to a time"
|
|
|
|
t = heure.strip().upper().replace("H", ":")
|
|
|
|
h, m = t.split(":")[:2]
|
|
|
|
return datetime.time(int(h), int(m))
|
|
|
|
|
|
|
|
|
|
|
|
def _moduleimpl_evaluation_insert_before(
|
|
|
|
evaluations: list[Evaluation], next_eval: Evaluation
|
|
|
|
) -> int:
|
|
|
|
"""Renumber evaluations such that an evaluation with can be inserted before next_eval
|
|
|
|
Returns numero suitable for the inserted evaluation
|
|
|
|
"""
|
|
|
|
if next_eval:
|
|
|
|
n = next_eval.numero
|
|
|
|
if n is None:
|
|
|
|
Evaluation.moduleimpl_evaluation_renumber(next_eval.moduleimpl)
|
|
|
|
n = next_eval.numero
|
|
|
|
else:
|
|
|
|
n = 1
|
|
|
|
# all numeros >= n are incremented
|
|
|
|
for e in evaluations:
|
|
|
|
if e.numero >= n:
|
|
|
|
e.numero += 1
|
|
|
|
db.session.add(e)
|
|
|
|
db.session.commit()
|
|
|
|
return n
|
2024-02-25 22:35:14 +01:00
|
|
|
|
|
|
|
|
|
|
|
from app.models.moduleimpls import ModuleImpl, ModuleImplInscription
|