From ca04f3d5cb321ef567573dc97c1415fcee3bd2be Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet <emmanuel.viennet@gmail.com>
Date: Tue, 22 Aug 2023 17:02:00 +0200
Subject: [PATCH] WIP: modernisation evaluations
---
app/api/evaluations.py | 69 +++-
app/but/bulletin_but.py | 11 +-
app/but/bulletin_but_xml_compat.py | 19 +-
app/models/evaluations.py | 324 ++++++++++++++----
app/models/moduleimpls.py | 17 +
app/scodoc/notesdb.py | 23 +-
app/scodoc/sco_bulletins_json.py | 29 +-
app/scodoc/sco_bulletins_xml.py | 13 +-
app/scodoc/sco_evaluation_db.py | 180 +---------
app/scodoc/sco_evaluation_edit.py | 9 +-
app/scodoc/sco_moduleimpl_status.py | 3 +-
app/scodoc/sco_permissions_check.py | 28 --
app/scodoc/sco_ue_external.py | 17 +-
app/scodoc/sco_utils.py | 3 +
app/views/notes.py | 42 ++-
.../versions/5c44d0d215ca_evaluation_date.py | 58 ++++
scodoc.py | 3 +-
tests/api/exemple-api-basic.py | 9 +-
tests/api/exemple-api-scodoc7.py | 34 --
tests/api/test_api_evaluations.py | 76 ++--
tests/unit/sco_fake_gen.py | 22 +-
.../fakedatabase/create_test_api_database.py | 15 +-
tools/test_api.sh | 9 +-
23 files changed, 604 insertions(+), 409 deletions(-)
create mode 100644 migrations/versions/5c44d0d215ca_evaluation_date.py
diff --git a/app/api/evaluations.py b/app/api/evaluations.py
index af1c40af3..7fd84c539 100644
--- a/app/api/evaluations.py
+++ b/app/api/evaluations.py
@@ -7,17 +7,19 @@
"""
ScoDoc 9 API : accès aux évaluations
"""
+import datetime
from flask import g, request
from flask_json import as_json
-from flask_login import login_required
+from flask_login import current_user, login_required
import app
-
+from app import db
from app.api import api_bp as bp, api_web_bp
from app.decorators import scodoc, permission_required
from app.models import Evaluation, ModuleImpl, FormSemestre
-from app.scodoc import sco_evaluation_db, sco_saisie_notes
+from app.scodoc import sco_evaluation_db, sco_permissions_check, sco_saisie_notes
+from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
from app.scodoc.sco_permissions import Permission
import app.scodoc.sco_utils as scu
@@ -181,3 +183,64 @@ def evaluation_set_notes(evaluation_id: int):
return sco_saisie_notes.save_notes(
evaluation, notes, comment=data.get("comment", "")
)
+
+
+@bp.route("/moduleimpl/<int:moduleimpl_id>/evaluation/create", methods=["POST"])
+@api_web_bp.route("/moduleimpl/<int:moduleimpl_id>/evaluation/create", methods=["POST"])
+@login_required
+@scodoc
+@permission_required(Permission.ScoEnsView) # permission gérée dans la fonction
+@as_json
+def evaluation_create(moduleimpl_id: int):
+ """Création d'une évaluation.
+ The request content type should be "application/json",
+ and contains:
+ {
+ "description" : str,
+ "evaluation_type" : int, // {0,1,2} default 0 (normale)
+ "jour" : date_iso, // si non spécifié, vide
+ "date_debut" : date_iso, // optionnel
+ "date_fin" : date_iso, // si non spécifié, 08:00
+ "note_max" : float, // si non spécifié, 20.0
+ "numero" : int, // ordre de présentation, default tri sur date
+ "visibulletin" : boolean , //default true
+ "publish_incomplete" : boolean , //default false
+ "coefficient" : float, // si non spécifié, 1.0
+ "poids" : [ {
+ "ue_id": int,
+ "poids": float
+ },
+ ...
+ ] // si non spécifié, tous les poids à 1.0
+ }
+ }
+ Result: l'évaluation créée.
+ """
+ moduleimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id)
+ if not moduleimpl.can_edit_evaluation(current_user):
+ return scu.json_error(403, "opération non autorisée")
+ data = request.get_json(force=True) # may raise 400 Bad Request
+
+ try:
+ evaluation = Evaluation.create(moduleimpl=moduleimpl, **data)
+ except AccessDenied:
+ return scu.json_error(403, "opération non autorisée (2)")
+ except ValueError:
+ return scu.json_error(400, "paramètre incorrect")
+ except ScoValueError as exc:
+ breakpoint() # XXX WIP
+ return scu.json_error(400, f"paramètre de type incorrect ({exc.msg})")
+
+ db.session.add(evaluation)
+ db.session.commit()
+ return evaluation.to_dict_api()
+
+
+@bp.route("/evaluation/<int:evaluation_id>/delete", methods=["POST"])
+@api_web_bp.route("/evaluation/<int:evaluation_id>/delete", methods=["POST"])
+@login_required
+@scodoc
+@permission_required(Permission.ScoEnsView) # permission gérée dans la fonction
+@as_json
+def evaluation_delete(evaluation_id: int):
+ pass
diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py
index 3231cc88e..b9261c4fb 100644
--- a/app/but/bulletin_but.py
+++ b/app/but/bulletin_but.py
@@ -276,11 +276,10 @@ class BulletinBUT:
"coef": fmt_note(e.coefficient)
if e.evaluation_type == scu.EVALUATION_NORMALE
else None,
- "date": e.jour.isoformat() if e.jour else None,
+ "date_debut": e.date_debut.isoformat() if e.date_debut else None,
+ "date_fin": e.date_fin.isoformat() if e.date_fin else None,
"description": e.description,
"evaluation_type": e.evaluation_type,
- "heure_debut": e.heure_debut.strftime("%H:%M") if e.heure_debut else None,
- "heure_fin": e.heure_fin.strftime("%H:%M") if e.heure_debut else None,
"note": {
"value": fmt_note(
eval_notes[etud.id],
@@ -298,6 +297,12 @@ class BulletinBUT:
)
if has_request_context()
else "na",
+ # deprecated
+ "date": e.date_debut.isoformat() if e.date_debut else None,
+ "heure_debut": e.date_debut.time().isoformat("minutes")
+ if e.date_debut
+ else None,
+ "heure_fin": e.date_fin.time().isoformat("minutes") if e.date_fin else None,
}
return d
diff --git a/app/but/bulletin_but_xml_compat.py b/app/but/bulletin_but_xml_compat.py
index 0bb1b9021..faf932e6f 100644
--- a/app/but/bulletin_but_xml_compat.py
+++ b/app/but/bulletin_but_xml_compat.py
@@ -202,12 +202,11 @@ def bulletin_but_xml_compat(
if e.visibulletin or version == "long":
x_eval = Element(
"evaluation",
- jour=e.jour.isoformat() if e.jour else "",
- heure_debut=e.heure_debut.isoformat()
- if e.heure_debut
+ date_debut=e.date_debut.isoformat()
+ if e.date_debut
else "",
- heure_fin=e.heure_fin.isoformat()
- if e.heure_debut
+ date_fin=e.date_fin.isoformat()
+ if e.date_debut
else "",
coefficient=str(e.coefficient),
# pas les poids en XML compat
@@ -215,6 +214,16 @@ def bulletin_but_xml_compat(
description=quote_xml_attr(e.description),
# notes envoyées sur 20, ceci juste pour garder trace:
note_max_origin=str(e.note_max),
+ # --- deprecated
+ jour=e.date_debut.isoformat()
+ if e.date_debut
+ else "",
+ heure_debut=e.date_debut.time().isoformat("minutes")
+ if e.date_debut
+ else "",
+ heure_fin=e.date_fin.time().isoformat("minutes")
+ if e.date_fin
+ else "",
)
x_mod.append(x_eval)
try:
diff --git a/app/models/evaluations.py b/app/models/evaluations.py
index 987b51706..deb4bc263 100644
--- a/app/models/evaluations.py
+++ b/app/models/evaluations.py
@@ -5,17 +5,28 @@
import datetime
from operator import attrgetter
+from flask import g, url_for
+from flask_login import current_user
+import sqlalchemy as sa
+
from app import db
from app.models.etudiants import Identite
+from app.models.events import ScolarNews
from app.models.moduleimpls import ModuleImpl
from app.models.notes import NotesNotes
from app.models.ues import UniteEns
-from app.scodoc.sco_exceptions import ScoValueError
+from app.scodoc import sco_cache
+from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
import app.scodoc.notesdb as ndb
+import app.scodoc.sco_utils as scu
+MAX_EVALUATION_DURATION = datetime.timedelta(days=365)
+NOON = datetime.time(12, 00)
DEFAULT_EVALUATION_TIME = datetime.time(8, 0)
+VALID_EVALUATION_TYPES = {0, 1, 2}
+
class Evaluation(db.Model):
"""Evaluation (contrôle, examen, ...)"""
@@ -27,9 +38,8 @@ class Evaluation(db.Model):
moduleimpl_id = db.Column(
db.Integer, db.ForeignKey("notes_moduleimpl.id"), index=True
)
- jour = db.Column(db.Date)
- heure_debut = db.Column(db.Time)
- heure_fin = db.Column(db.Time)
+ 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)
@@ -50,47 +60,106 @@ class Evaluation(db.Model):
def __repr__(self):
return f"""<Evaluation {self.id} {
- self.jour.isoformat() if self.jour else ''} "{
+ self.date_debut.isoformat() if self.date_debut else ''} "{
self.description[:16] if self.description else ''}">"""
+ @classmethod
+ def create(
+ cls,
+ moduleimpl: ModuleImpl = None,
+ jour=None,
+ heure_debut=None,
+ heure_fin=None,
+ description=None,
+ note_max=None,
+ coefficient=None,
+ visibulletin=None,
+ publish_incomplete=None,
+ evaluation_type=None,
+ numero=None,
+ **kw, # ceci pour absorber les éventuel arguments excedentaires
+ ):
+ """Create an evaluation. Check permission and all arguments."""
+ 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"]
+ check_convert_evaluation_args(moduleimpl, args)
+ # 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)
+ sco_cache.invalidate_formsemestre(formsemestre_id=moduleimpl.formsemestre_id)
+ url = url_for(
+ "notes.moduleimpl_status",
+ scodoc_dept=g.scodoc_dept,
+ moduleimpl_id=moduleimpl.id,
+ )
+ ScolarNews.add(
+ typ=ScolarNews.NEWS_NOTE,
+ obj=moduleimpl.id,
+ text=f"""Création d'une évaluation dans <a href="{url}">{
+ moduleimpl.module.titre or '(module sans titre)'}</a>""",
+ url=url,
+ )
+ return evaluation
+
+ @classmethod
+ def get_new_numero(
+ cls, moduleimpl: ModuleImpl, date_debut: datetime.datetime
+ ) -> int:
+ """Get a new numero for an evaluation in this moduleimpl
+ If necessary, renumber existing evals to make room for a new one.
+ """
+ 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:
+ if e.date_debut > t:
+ 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
+
def to_dict(self) -> dict:
"Représentation dict (riche, compat ScoDoc 7)"
e = dict(self.__dict__)
e.pop("_sa_instance_state", None)
# ScoDoc7 output_formators
e["evaluation_id"] = self.id
- e["jour"] = e["jour"].strftime("%d/%m/%Y") if e["jour"] else ""
- if self.jour is None:
- e["date_debut"] = None
- e["date_fin"] = None
- else:
- e["date_debut"] = datetime.datetime.combine(
- self.jour, self.heure_debut or datetime.time(0, 0)
- ).isoformat()
- e["date_fin"] = datetime.datetime.combine(
- self.jour, self.heure_fin or datetime.time(0, 0)
- ).isoformat()
+ e["date_debut"] = e.date_debut.isoformat() if e.date_debut else None
+ e["date_fin"] = e.date_debut.isoformat() if e.date_fin else None
e["numero"] = ndb.int_null_is_zero(e["numero"])
e["poids"] = self.get_ue_poids_dict() # { ue_id : poids }
+
+ # Deprecated
+ e["jour"] = e.date_debut.strftime("%d/%m/%Y") if e.date_debut else ""
+
return evaluation_enrich_dict(e)
def to_dict_api(self) -> dict:
"Représentation dict pour API JSON"
- if self.jour is None:
- date_debut = None
- date_fin = None
- else:
- date_debut = datetime.datetime.combine(
- self.jour, self.heure_debut or datetime.time(0, 0)
- ).isoformat()
- date_fin = datetime.datetime.combine(
- self.jour, self.heure_fin or datetime.time(0, 0)
- ).isoformat()
-
return {
"coefficient": self.coefficient,
- "date_debut": date_debut,
- "date_fin": date_fin,
+ "date_debut": self.date_debut.isoformat(),
+ "date_fin": self.date_fin.isoformat(),
"description": self.description,
"evaluation_type": self.evaluation_type,
"id": self.id,
@@ -104,11 +173,49 @@ class Evaluation(db.Model):
def from_dict(self, data):
"""Set evaluation attributes from given dict values."""
- check_evaluation_args(data)
+ check_convert_evaluation_args(self.moduleimpl, data)
+ if data.get("numero") is None:
+ data["numero"] = Evaluation.get_max_numero() + 1
for k in self.__dict__.keys():
if k != "_sa_instance_state" and k != "id" and k in data:
setattr(self, k, data[k])
+ @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(
+ cls, moduleimpl: ModuleImpl, only_if_unumbered=False
+ ):
+ """Renumber evaluations in this moduleimpl, according to their date. (numero=0: oldest one)
+ Needed because previous versions of ScoDoc did not have eval numeros
+ 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()
+ all_numbered = all(e.numero is not None for e in evaluations)
+ 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()
+
def descr_heure(self) -> str:
"Description de la plage horaire pour affichages"
if self.heure_debut and (
@@ -146,19 +253,19 @@ class Evaluation(db.Model):
return copy
def is_matin(self) -> bool:
- "Evaluation ayant lieu le matin (faux si pas de date)"
- heure_debut_dt = self.heure_debut or datetime.time(8, 00)
- # 8:00 au cas ou pas d'heure (note externe?)
- return bool(self.jour) and heure_debut_dt < datetime.time(12, 00)
+ "Evaluation commençant le matin (faux si pas de date)"
+ if not self.date_debut:
+ return False
+ return self.date_debut.time() < NOON
def is_apresmidi(self) -> bool:
- "Evaluation ayant lieu l'après midi (faux si pas de date)"
- heure_debut_dt = self.heure_debut or datetime.time(8, 00)
- # 8:00 au cas ou pas d'heure (note externe?)
- return bool(self.jour) and heure_debut_dt >= datetime.time(12, 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
def set_default_poids(self) -> bool:
- """Initialize les poids bvers 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.
Les poids existants ne sont pas modifiés.
Return True if (uncommited) modification, False otherwise.
@@ -278,15 +385,13 @@ class EvaluationUEPoids(db.Model):
def evaluation_enrich_dict(e: dict):
"""add or convert some fields in an evaluation dict"""
# For ScoDoc7 compat
- heure_debut_dt = e["heure_debut"] or datetime.time(
- 8, 00
- ) # au cas ou pas d'heure (note externe?)
- 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"])
- e["jour_iso"] = ndb.DateDMYtoISO(e["jour"])
+ heure_debut_dt = e["date_debut"].time()
+ heure_fin_dt = e["date_fin"].time()
+ e["heure_debut"] = heure_debut_dt.strftime("%Hh%M")
+ e["heure_fin"] = heure_fin_dt.strftime("%Hh%M")
+ e["jour_iso"] = e["date_debut"].isoformat() # XXX
heure_debut, heure_fin = e["heure_debut"], e["heure_fin"]
- d = ndb.TimeDuration(heure_debut, heure_fin)
+ d = _time_duration_HhM(heure_debut, heure_fin)
if d is not None:
m = d % 60
e["duree"] = "%dh" % (d / 60)
@@ -313,49 +418,118 @@ def evaluation_enrich_dict(e: dict):
return e
-def check_evaluation_args(args):
- "Check coefficient, dates and duration, raises exception if invalid"
- moduleimpl_id = args["moduleimpl_id"]
- # check bareme
- note_max = args.get("note_max", None)
- if note_max is None:
- raise ScoValueError("missing note_max")
+def check_convert_evaluation_args(moduleimpl: ModuleImpl, data: dict):
+ """Check coefficient, dates and duration, raises exception if invalid.
+ Convert date and time strings to date and time objects.
+
+ Set required default value for unspecified fields.
+ May raise ScoValueError.
+ """
+ # --- description
+ description = data.get("description", "")
+ if len(description) > scu.MAX_TEXT_LEN:
+ raise ScoValueError("description too large")
+ # --- evaluation_type
+ try:
+ data["evaluation_type"] = int(data.get("evaluation_type", 0) or 0)
+ if not data["evaluation_type"] in VALID_EVALUATION_TYPES:
+ raise ScoValueError("Invalid evaluation_type value")
+ except ValueError:
+ raise ScoValueError("Invalid evaluation_type value")
+
+ # --- note_max (bareme)
+ note_max = data.get("note_max", 20.0) or 20.0
try:
note_max = float(note_max)
except ValueError:
raise ScoValueError("Invalid note_max value")
if note_max < 0:
raise ScoValueError("Invalid note_max value (must be positive or null)")
- # check coefficient
- coef = args.get("coefficient", None)
- if coef is None:
- raise ScoValueError("missing coefficient")
+ data["note_max"] = note_max
+ # --- coefficient
+ coef = data.get("coefficient", 1.0) or 1.0
try:
coef = float(coef)
except ValueError:
raise ScoValueError("Invalid coefficient value")
if coef < 0:
raise ScoValueError("Invalid coefficient value (must be positive or null)")
- # check date
- jour = args.get("jour", None)
- args["jour"] = jour
- if jour:
- modimpl = db.session.get(ModuleImpl, moduleimpl_id)
- formsemestre = modimpl.formsemestre
- y, m, d = [int(x) for x in ndb.DateDMYtoISO(jour).split("-")]
- jour = datetime.date(y, m, d)
+ data["coefficient"] = coef
+ # --- jour (date de l'évaluation)
+ jour = data.get("jour", None)
+ if jour and not isinstance(jour, datetime.date):
+ if date_format == "dmy":
+ y, m, d = [int(x) for x in ndb.DateDMYtoISO(jour).split("-")]
+ jour = datetime.date(y, m, d)
+ else: # ISO
+ jour = datetime.date.fromisoformat(jour)
+ formsemestre = moduleimpl.formsemestre
if (jour > formsemestre.date_fin) or (jour < formsemestre.date_debut):
raise ScoValueError(
- "La date de l'évaluation (%s/%s/%s) n'est pas dans le semestre !"
- % (d, m, y),
+ f"""La date de l'évaluation ({jour.strftime("%d/%m/%Y")}) n'est pas dans le semestre !""",
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
+ data["jour"] = jour
+ # --- heures
+ heure_debut = data.get("heure_debut", None)
+ if heure_debut and not isinstance(heure_debut, datetime.time):
+ if date_format == "dmy":
+ data["heure_debut"] = heure_to_time(heure_debut)
+ else: # ISO
+ data["heure_debut"] = datetime.time.fromisoformat(heure_debut)
+ heure_fin = data.get("heure_fin", None)
+ if heure_fin and not isinstance(heure_fin, datetime.time):
+ if date_format == "dmy":
+ data["heure_fin"] = heure_to_time(heure_fin)
+ else: # ISO
+ data["heure_fin"] = datetime.time.fromisoformat(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 !")
+ if heure_debut and heure_fin:
+ duration = ((data["heure_fin"].hour * 60) + data["heure_fin"].minute) - (
+ (data["heure_debut"].hour * 60) + data["heure_debut"].minute
+ )
+ if duration < 0 or duration > 60 * 12:
+ raise ScoValueError("Heures de l'évaluation incohérentes !")
+
+
+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 _time_duration_HhM(heure_debut: str, heure_fin: str) -> int:
+ """duree (nb entier de minutes) entre deux heures a notre format
+ ie 12h23
+ """
+ if heure_debut and heure_fin:
+ h0, m0 = [int(x) for x in heure_debut.split("h")]
+ h1, m1 = [int(x) for x in heure_fin.split("h")]
+ d = (h1 - h0) * 60 + (m1 - m0)
+ return d
+ else:
+ return None
+
+
+def _moduleimpl_evaluation_insert_before(
+ evaluations: list[Evaluation], next_eval: Evaluation
+) -> int:
+ """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
diff --git a/app/models/moduleimpls.py b/app/models/moduleimpls.py
index c4d7c3fe0..e84fe82b3 100644
--- a/app/models/moduleimpls.py
+++ b/app/models/moduleimpls.py
@@ -101,6 +101,23 @@ class ModuleImpl(db.Model):
d.pop("module", None)
return d
+ def can_edit_evaluation(self, user) -> bool:
+ """True if this user can create, delete or edit and evaluation in this modimpl
+ (nb: n'implique pas le droit de saisir ou modifier des notes)
+ """
+ # acces pour resp. moduleimpl et resp. form semestre (dir etud)
+ if (
+ user.has_permission(Permission.ScoEditAllEvals)
+ or user.id == self.responsable_id
+ or user.id in (r.id for r in self.formsemestre.responsables)
+ ):
+ return True
+ elif self.formsemestre.ens_can_edit_eval:
+ if user.id in (e.id for e in self.enseignants):
+ return True
+
+ return False
+
def can_change_ens_by(self, user: User, raise_exc=False) -> bool:
"""Check if user can modify module resp.
If raise_exc, raises exception (AccessDenied or ScoLockedSemError) if not.
diff --git a/app/scodoc/notesdb.py b/app/scodoc/notesdb.py
index 4d9b6aa2a..3b7dd2654 100644
--- a/app/scodoc/notesdb.py
+++ b/app/scodoc/notesdb.py
@@ -459,8 +459,10 @@ def dictfilter(d, fields, filter_nulls=True):
# --- Misc Tools
-def DateDMYtoISO(dmy: str, null_is_empty=False) -> str:
- "convert date string from french format to ISO"
+def DateDMYtoISO(dmy: str, null_is_empty=False) -> str: # XXX deprecated
+ """Convert date string from french format to ISO.
+ If null_is_empty (default false), returns "" if no input.
+ """
if not dmy:
if null_is_empty:
return ""
@@ -506,7 +508,7 @@ def DateISOtoDMY(isodate):
return "%02d/%02d/%04d" % (day, month, year)
-def TimetoISO8601(t, null_is_empty=False):
+def TimetoISO8601(t, null_is_empty=False) -> str:
"convert time string to ISO 8601 (allow 16:03, 16h03, 16)"
if isinstance(t, datetime.time):
return t.isoformat()
@@ -518,7 +520,7 @@ def TimetoISO8601(t, null_is_empty=False):
return t
-def TimefromISO8601(t):
+def TimefromISO8601(t) -> str:
"convert time string from ISO 8601 to our display format"
if not t:
return t
@@ -532,19 +534,6 @@ def TimefromISO8601(t):
return fs[0] + "h" + fs[1] # discard seconds
-def TimeDuration(heure_debut, heure_fin):
- """duree (nb entier de minutes) entre deux heures a notre format
- ie 12h23
- """
- if heure_debut and heure_fin:
- h0, m0 = [int(x) for x in heure_debut.split("h")]
- h1, m1 = [int(x) for x in heure_fin.split("h")]
- d = (h1 - h0) * 60 + (m1 - m0)
- return d
- else:
- return None
-
-
def float_null_is_zero(x):
if x is None or x == "":
return 0.0
diff --git a/app/scodoc/sco_bulletins_json.py b/app/scodoc/sco_bulletins_json.py
index 72661afc8..74bc29107 100644
--- a/app/scodoc/sco_bulletins_json.py
+++ b/app/scodoc/sco_bulletins_json.py
@@ -37,7 +37,7 @@ from app import db, ScoDocJSONEncoder
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import but_validations
-from app.models import Matiere, ModuleImpl, UniteEns
+from app.models import Evaluation, Matiere, ModuleImpl, UniteEns
from app.models.etudiants import Identite
from app.models.formsemestre import FormSemestre
@@ -324,7 +324,7 @@ def formsemestre_bulletinetud_published_dict(
def _list_modimpls(
nt: NotesTableCompat,
etudid: int,
- modimpls: list[ModuleImpl],
+ modimpls: list[dict],
prefs: SemPreferences,
version: str,
) -> list[dict]:
@@ -398,24 +398,29 @@ def _list_modimpls(
# Evaluations incomplètes ou futures:
complete_eval_ids = set([e["evaluation_id"] for e in evals])
if prefs["bul_show_all_evals"]:
- all_evals = sco_evaluation_db.do_evaluation_list(
- args={"moduleimpl_id": modimpl["moduleimpl_id"]}
- )
- all_evals.reverse() # plus ancienne d'abord
- for e in all_evals:
- if e["evaluation_id"] not in complete_eval_ids:
+ evaluations = Evaluation.query.filter_by(
+ moduleimpl_id=modimpl["moduleimpl_id"]
+ ).order_by(Evaluation.date_debut)
+ # plus ancienne d'abord
+ for e in evaluations:
+ if e.id not in complete_eval_ids:
mod_dict["evaluation"].append(
dict(
- jour=ndb.DateDMYtoISO(e["jour"], null_is_empty=True),
+ date_debut=e.date_debut.isoformat()
+ if e.date_debut
+ else None,
+ date_fin=e.date_fin.isoformat() if e.date_fin else None,
+ coefficient=e.coefficient,
+ description=quote_xml_attr(e.description or ""),
+ incomplete="1",
+ # Deprecated:
+ jour=e.date_debut.isoformat() if e.date_debut else "",
heure_debut=ndb.TimetoISO8601(
e["heure_debut"], null_is_empty=True
),
heure_fin=ndb.TimetoISO8601(
e["heure_fin"], null_is_empty=True
),
- coefficient=e["coefficient"],
- description=quote_xml_attr(e["description"]),
- incomplete="1",
)
)
modules_dict.append(mod_dict)
diff --git a/app/scodoc/sco_bulletins_xml.py b/app/scodoc/sco_bulletins_xml.py
index 178b02964..ca32ec46a 100644
--- a/app/scodoc/sco_bulletins_xml.py
+++ b/app/scodoc/sco_bulletins_xml.py
@@ -50,11 +50,11 @@ import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app import log
from app.but.bulletin_but_xml_compat import bulletin_but_xml_compat
+from app.models.evaluations import Evaluation
from app.models.formsemestre import FormSemestre
from app.scodoc import sco_assiduites
from app.scodoc import codes_cursus
from app.scodoc import sco_edit_ue
-from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups
from app.scodoc import sco_photos
@@ -320,12 +320,11 @@ def make_xml_formsemestre_bulletinetud(
if sco_preferences.get_preference(
"bul_show_all_evals", formsemestre_id
):
- all_evals = sco_evaluation_db.do_evaluation_list(
- args={"moduleimpl_id": modimpl["moduleimpl_id"]}
- )
- all_evals.reverse() # plus ancienne d'abord
- for e in all_evals:
- if e["evaluation_id"] not in complete_eval_ids:
+ evaluations = Evaluation.query.filter_by(
+ moduleimpl_id=modimpl["moduleimpl_id"]
+ ).order_by(Evaluation.date_debut)
+ for e in evaluations:
+ if e.id not in complete_eval_ids:
x_eval = Element(
"evaluation",
jour=ndb.DateDMYtoISO(e["jour"], null_is_empty=True),
diff --git a/app/scodoc/sco_evaluation_db.py b/app/scodoc/sco_evaluation_db.py
index 260481b99..f0af65eac 100644
--- a/app/scodoc/sco_evaluation_db.py
+++ b/app/scodoc/sco_evaluation_db.py
@@ -37,7 +37,7 @@ from flask_login import current_user
from app import db, log
from app.models import Evaluation, ModuleImpl, ScolarNews
-from app.models.evaluations import evaluation_enrich_dict, check_evaluation_args
+from app.models.evaluations import evaluation_enrich_dict, check_convert_evaluation_args
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
@@ -89,7 +89,6 @@ def do_evaluation_list(args, sortkey=None):
'apresmidi' : 1 (termine après 12:00) ou 0
'descrheure' : ' de 15h00 à 16h30'
"""
- # Attention: transformation fonction ScoDoc7 en SQLAlchemy
cnx = ndb.GetDBConnexion()
evals = _evaluationEditor.list(cnx, args, sortkey=sortkey)
# calcule duree (chaine de car.) de chaque evaluation et ajoute jour_iso, matin, apresmidi
@@ -108,115 +107,33 @@ def do_evaluation_list_in_formsemestre(formsemestre_id):
return evals
-def do_evaluation_create(
- moduleimpl_id=None,
- jour=None,
- heure_debut=None,
- heure_fin=None,
- description=None,
- note_max=None,
- coefficient=None,
- visibulletin=None,
- publish_incomplete=None,
- evaluation_type=None,
- numero=None,
- **kw, # ceci pour absorber les arguments excedentaires de tf #sco8
-):
- """Create an evaluation"""
- if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id):
- raise AccessDenied(
- f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
- )
- args = locals()
- log("do_evaluation_create: args=" + str(args))
- modimpl: ModuleImpl = db.session.get(ModuleImpl, moduleimpl_id)
- if modimpl is None:
- raise ValueError("module not found")
- check_evaluation_args(args)
- # Check numeros
- moduleimpl_evaluation_renumber(moduleimpl_id, only_if_unumbered=True)
- if not "numero" in args or args["numero"] is None:
- n = None
- # determine le numero avec la date
- # Liste des eval existantes triees par date, la plus ancienne en tete
- mod_evals = do_evaluation_list(
- args={"moduleimpl_id": moduleimpl_id},
- sortkey="jour asc, heure_debut asc",
- )
- if args["jour"]:
- next_eval = None
- t = (
- ndb.DateDMYtoISO(args["jour"], null_is_empty=True),
- ndb.TimetoISO8601(args["heure_debut"], null_is_empty=True),
- )
- for e in mod_evals:
- if (
- ndb.DateDMYtoISO(e["jour"], null_is_empty=True),
- ndb.TimetoISO8601(e["heure_debut"], null_is_empty=True),
- ) > t:
- next_eval = e
- break
- if next_eval:
- n = moduleimpl_evaluation_insert_before(mod_evals, next_eval)
- else:
- n = None # a placer en fin
- if n is None: # pas de date ou en fin:
- if mod_evals:
- log(pprint.pformat(mod_evals[-1]))
- n = mod_evals[-1]["numero"] + 1
- else:
- n = 0 # the only one
- # log("creating with numero n=%d" % n)
- args["numero"] = n
-
- #
- cnx = ndb.GetDBConnexion()
- r = _evaluationEditor.create(cnx, args)
-
- # news
- sco_cache.invalidate_formsemestre(formsemestre_id=modimpl.formsemestre_id)
- url = url_for(
- "notes.moduleimpl_status",
- scodoc_dept=g.scodoc_dept,
- moduleimpl_id=moduleimpl_id,
- )
- ScolarNews.add(
- typ=ScolarNews.NEWS_NOTE,
- obj=moduleimpl_id,
- text=f"""Création d'une évaluation dans <a href="{url}">{
- modimpl.module.titre or '(module sans titre)'}</a>""",
- url=url,
- )
-
- return r
-
-
def do_evaluation_edit(args):
"edit an evaluation"
evaluation_id = args["evaluation_id"]
- the_evals = do_evaluation_list({"evaluation_id": evaluation_id})
- if not the_evals:
+ evaluation: Evaluation = db.session.get(Evaluation, evaluation_id)
+ if evaluation is None:
raise ValueError("evaluation inexistante !")
- moduleimpl_id = the_evals[0]["moduleimpl_id"]
- if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id):
+
+ if not evaluation.moduleimpl.can_edit_evaluation(current_user):
raise AccessDenied(
- "Modification évaluation impossible pour %s" % current_user.get_nomplogin()
+ f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
)
- args["moduleimpl_id"] = moduleimpl_id
- check_evaluation_args(args)
+ args["moduleimpl_id"] = evaluation.moduleimpl.id
+ check_convert_evaluation_args(evaluation.moduleimpl, args)
cnx = ndb.GetDBConnexion()
_evaluationEditor.edit(cnx, args)
# inval cache pour ce semestre
- M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
- sco_cache.invalidate_formsemestre(formsemestre_id=M["formsemestre_id"])
+ sco_cache.invalidate_formsemestre(
+ formsemestre_id=evaluation.moduleimpl.formsemestre_id
+ )
def do_evaluation_delete(evaluation_id):
"delete evaluation"
evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id)
modimpl: ModuleImpl = evaluation.moduleimpl
- if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=modimpl.id):
+ if not modimpl.can_edit_evaluation(current_user):
raise AccessDenied(
f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
)
@@ -287,68 +204,6 @@ def do_evaluation_get_all_notes(
return d
-def moduleimpl_evaluation_renumber(moduleimpl_id, only_if_unumbered=False, redirect=0):
- """Renumber evaluations in this module, according to their date. (numero=0: oldest one)
- Needed because previous versions of ScoDoc did not have eval numeros
- Note: existing numeros are ignored
- """
- redirect = int(redirect)
- # log('moduleimpl_evaluation_renumber( moduleimpl_id=%s )' % moduleimpl_id )
- # List sorted according to date/heure, ignoring numeros:
- # (note that we place evaluations with NULL date at the end)
- mod_evals = do_evaluation_list(
- args={"moduleimpl_id": moduleimpl_id},
- sortkey="jour asc, heure_debut asc",
- )
-
- all_numbered = False not in [x["numero"] > 0 for x in mod_evals]
- if all_numbered and only_if_unumbered:
- return # all ok
-
- # Reset all numeros:
- i = 1
- for e in mod_evals:
- e["numero"] = i
- do_evaluation_edit(e)
- i += 1
-
- # If requested, redirect to moduleimpl page:
- if redirect:
- return flask.redirect(
- url_for(
- "notes.moduleimpl_status",
- scodoc_dept=g.scodoc_dept,
- moduleimpl_id=moduleimpl_id,
- )
- )
-
-
-def moduleimpl_evaluation_insert_before(mod_evals, next_eval):
- """Renumber evals 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 not n:
- log("renumbering old evals")
- moduleimpl_evaluation_renumber(next_eval["moduleimpl_id"])
- next_eval = do_evaluation_list(
- args={"evaluation_id": next_eval["evaluation_id"]}
- )[0]
- n = next_eval["numero"]
- else:
- n = 1
- # log('inserting at position numero %s' % n )
- # all numeros >= n are incremented
- for e in mod_evals:
- if e["numero"] >= n:
- e["numero"] += 1
- # log('incrementing %s to %s' % (e['evaluation_id'], e['numero']))
- do_evaluation_edit(e)
-
- return n
-
-
def moduleimpl_evaluation_move(evaluation_id: int, after=0, redirect=1):
"""Move before/after previous one (decrement/increment numero)
(published)
@@ -357,12 +212,13 @@ def moduleimpl_evaluation_move(evaluation_id: int, after=0, redirect=1):
moduleimpl_id = evaluation.moduleimpl_id
redirect = int(redirect)
# access: can change eval ?
- if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id):
+ if not evaluation.moduleimpl.can_edit_evaluation(current_user):
raise AccessDenied(
f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
)
-
- moduleimpl_evaluation_renumber(moduleimpl_id, only_if_unumbered=True)
+ Evaluation.moduleimpl_evaluation_renumber(
+ evaluation.moduleimpl, only_if_unumbered=True
+ )
e = do_evaluation_list(args={"evaluation_id": evaluation_id})[0]
after = int(after) # 0: deplace avant, 1 deplace apres
@@ -379,8 +235,8 @@ def moduleimpl_evaluation_move(evaluation_id: int, after=0, redirect=1):
if neigh: #
if neigh["numero"] == e["numero"]:
log("Warning: moduleimpl_evaluation_move: forcing renumber")
- moduleimpl_evaluation_renumber(
- e["moduleimpl_id"], only_if_unumbered=False
+ Evaluation.moduleimpl_evaluation_renumber(
+ evaluation.moduleimpl, only_if_unumbered=False
)
else:
# swap numero with neighbor
diff --git a/app/scodoc/sco_evaluation_edit.py b/app/scodoc/sco_evaluation_edit.py
index ac9f6e2fa..f40a30e94 100644
--- a/app/scodoc/sco_evaluation_edit.py
+++ b/app/scodoc/sco_evaluation_edit.py
@@ -83,7 +83,7 @@ def evaluation_create_form(
can_edit_poids = not preferences["but_disable_edit_poids_evaluations"]
min_note_max = scu.NOTES_PRECISION # le plus petit bareme possible
#
- if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id):
+ if not modimpl.can_edit_evaluation(current_user):
return f"""
{html_sco_header.sco_header()}
<h2>Opération non autorisée</h2>
@@ -356,8 +356,11 @@ def evaluation_create_form(
if edit:
sco_evaluation_db.do_evaluation_edit(tf[2])
else:
- # création d'une evaluation (via fonction ScoDoc7)
- evaluation_id = sco_evaluation_db.do_evaluation_create(**tf[2])
+ # création d'une evaluation
+ evaluation = Evaluation.create(moduleimpl=modimpl, **tf[2])
+ db.session.add(evaluation)
+ db.session.commit()
+ evaluation_id = evaluation.id
if is_apc:
# Set poids
evaluation = db.session.get(Evaluation, evaluation_id)
diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py
index 84e5766ae..3f167cf02 100644
--- a/app/scodoc/sco_moduleimpl_status.py
+++ b/app/scodoc/sco_moduleimpl_status.py
@@ -435,8 +435,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
top_table_links += f"""
<a class="stdlink" style="margin-left:2em;" href="{
url_for("notes.moduleimpl_evaluation_renumber",
- scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id,
- redirect=1)
+ scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
}">Trier par date</a>
"""
if nb_evaluations > 0:
diff --git a/app/scodoc/sco_permissions_check.py b/app/scodoc/sco_permissions_check.py
index 3ff5d3a3d..495a1eb4b 100644
--- a/app/scodoc/sco_permissions_check.py
+++ b/app/scodoc/sco_permissions_check.py
@@ -54,34 +54,6 @@ def can_edit_notes(authuser, moduleimpl_id, allow_ens=True):
return True
-def can_edit_evaluation(moduleimpl_id=None):
- """Vérifie que l'on a le droit de modifier, créer ou détruire une
- évaluation dans ce module.
- Sinon, lance une exception.
- (nb: n'implique pas le droit de saisir ou modifier des notes)
- """
- from app.scodoc import sco_formsemestre
-
- # acces pour resp. moduleimpl et resp. form semestre (dir etud)
- if moduleimpl_id is None:
- raise ValueError("no moduleimpl specified") # bug
- M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
- sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
-
- if (
- current_user.has_permission(Permission.ScoEditAllEvals)
- or current_user.id == M["responsable_id"]
- or current_user.id in sem["responsables"]
- ):
- return True
- elif sem["ens_can_edit_eval"]:
- for ens in M["ens"]:
- if ens["ens_id"] == current_user.id:
- return True
-
- return False
-
-
def can_suppress_annotation(annotation_id):
"""True if current user can suppress this annotation
Seuls l'auteur de l'annotation et le chef de dept peuvent supprimer
diff --git a/app/scodoc/sco_ue_external.py b/app/scodoc/sco_ue_external.py
index 22471d3c4..07177a662 100644
--- a/app/scodoc/sco_ue_external.py
+++ b/app/scodoc/sco_ue_external.py
@@ -60,7 +60,7 @@ from app.models.formsemestre import FormSemestre
from app import db, log
-from app.models import UniteEns
+from app.models import Evaluation, ModuleImpl, UniteEns
from app.scodoc import html_sco_header
from app.scodoc import codes_cursus
from app.scodoc import sco_edit_matiere
@@ -154,6 +154,7 @@ def external_ue_inscrit_et_note(
"""Inscrit les étudiants au moduleimpl, crée au besoin une évaluation
et enregistre les notes.
"""
+ moduleimpl: ModuleImpl = db.session.get(ModuleImpl, moduleimpl_id)
log(
f"external_ue_inscrit_et_note(moduleimpl_id={moduleimpl_id}, notes_etuds={notes_etuds})"
)
@@ -163,18 +164,14 @@ def external_ue_inscrit_et_note(
formsemestre_id,
list(notes_etuds.keys()),
)
-
# Création d'une évaluation si il n'y en a pas déjà:
- mod_evals = sco_evaluation_db.do_evaluation_list(
- args={"moduleimpl_id": moduleimpl_id}
- )
- if len(mod_evals):
+ if moduleimpl.evaluations.count() > 0:
# met la note dans le première évaluation existante:
- evaluation_id = mod_evals[0]["evaluation_id"]
+ evaluation: Evaluation = moduleimpl.evaluations.first()
else:
# crée une évaluation:
- evaluation_id = sco_evaluation_db.do_evaluation_create(
- moduleimpl_id=moduleimpl_id,
+ evaluation: Evaluation = Evaluation.create(
+ moduleimpl=moduleimpl,
note_max=20.0,
coefficient=1.0,
publish_incomplete=True,
@@ -185,7 +182,7 @@ def external_ue_inscrit_et_note(
# Saisie des notes
_, _, _ = sco_saisie_notes.notes_add(
current_user,
- evaluation_id,
+ evaluation.id,
list(notes_etuds.items()),
do_it=True,
)
diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py
index 6208c16e1..25645cebd 100644
--- a/app/scodoc/sco_utils.py
+++ b/app/scodoc/sco_utils.py
@@ -68,6 +68,9 @@ from app.scodoc.codes_cursus import NOTES_TOLERANCE, CODES_EXPL
from app.scodoc import sco_xml
import sco_version
+# En principe, aucun champ text ne devrait excéder cette taille
+MAX_TEXT_LEN = 64 * 1024
+
# le répertoire static, lié à chaque release pour éviter les problèmes de caches
STATIC_DIR = (
os.environ.get("SCRIPT_NAME", "") + "/ScoDoc/static/links/" + sco_version.SCOVERSION
diff --git a/app/views/notes.py b/app/views/notes.py
index da80388a3..9f344575b 100644
--- a/app/views/notes.py
+++ b/app/views/notes.py
@@ -57,8 +57,8 @@ from app.but.forms import jury_but_forms
from app.comp import jury, res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import (
+ Evaluation,
Formation,
- ScolarFormSemestreValidation,
ScolarAutorisationInscription,
ScolarNews,
Scolog,
@@ -134,6 +134,7 @@ from app.scodoc import sco_lycee
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_moduleimpl_inscriptions
from app.scodoc import sco_moduleimpl_status
+from app.scodoc import sco_permissions_check
from app.scodoc import sco_placement
from app.scodoc import sco_poursuite_dut
from app.scodoc import sco_preferences
@@ -378,11 +379,40 @@ sco_publish(
sco_evaluations.formsemestre_evaluations_delai_correction,
Permission.ScoView,
)
-sco_publish(
- "/moduleimpl_evaluation_renumber",
- sco_evaluation_db.moduleimpl_evaluation_renumber,
- Permission.ScoView,
-)
+
+
+@bp.route("/moduleimpl_evaluation_renumber", methods=["GET", "POST"])
+@scodoc
+@permission_required_compat_scodoc7(Permission.ScoView)
+@scodoc7func
+def moduleimpl_evaluation_renumber(moduleimpl_id):
+ "Renumérote les évaluations, triant par date"
+ modimpl: ModuleImpl = (
+ ModuleImpl.query.filter_by(id=moduleimpl_id)
+ .join(FormSemestre)
+ .filter_by(dept_id=g.scodoc_dept_id)
+ .first_or_404()
+ )
+ if not modimpl.can_edit_evaluation(current_user):
+ raise ScoPermissionDenied(
+ dest_url=url_for(
+ "notes.moduleimpl_status",
+ scodoc_dept=g.scodoc_dept,
+ moduleimpl_id=modimpl.id,
+ )
+ )
+ Evaluation.moduleimpl_evaluation_renumber(modimpl)
+ # redirect to moduleimpl page:
+ if redirect:
+ return flask.redirect(
+ url_for(
+ "notes.moduleimpl_status",
+ scodoc_dept=g.scodoc_dept,
+ moduleimpl_id=moduleimpl_id,
+ )
+ )
+
+
sco_publish(
"/moduleimpl_evaluation_move",
sco_evaluation_db.moduleimpl_evaluation_move,
diff --git a/migrations/versions/5c44d0d215ca_evaluation_date.py b/migrations/versions/5c44d0d215ca_evaluation_date.py
new file mode 100644
index 000000000..66ab3617c
--- /dev/null
+++ b/migrations/versions/5c44d0d215ca_evaluation_date.py
@@ -0,0 +1,58 @@
+"""evaluation date: modifie le codage des dates d'évaluations
+
+Revision ID: 5c44d0d215ca
+Revises: 45e0a855b8eb
+Create Date: 2023-08-22 14:39:23.831483
+
+"""
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import postgresql
+
+# revision identifiers, used by Alembic.
+revision = "5c44d0d215ca"
+down_revision = "45e0a855b8eb"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ "modifie les colonnes codant les dates d'évaluations"
+ with op.batch_alter_table("notes_evaluation", schema=None) as batch_op:
+ batch_op.add_column(
+ sa.Column("date_debut", sa.DateTime(timezone=True), nullable=True)
+ )
+ batch_op.add_column(
+ sa.Column("date_fin", sa.DateTime(timezone=True), nullable=True)
+ )
+ # recode les dates existantes
+ op.execute("UPDATE notes_evaluation SET date_debut = jour+heure_debut;")
+ op.execute("UPDATE notes_evaluation SET date_fin = jour+heure_fin;")
+ with op.batch_alter_table("notes_evaluation", schema=None) as batch_op:
+ batch_op.drop_column("jour")
+ batch_op.drop_column("heure_fin")
+ batch_op.drop_column("heure_debut")
+
+
+def downgrade():
+ "modifie les colonnes codant les dates d'évaluations"
+ with op.batch_alter_table("notes_evaluation", schema=None) as batch_op:
+ batch_op.add_column(
+ sa.Column(
+ "heure_debut", postgresql.TIME(), autoincrement=False, nullable=True
+ )
+ )
+ batch_op.add_column(
+ sa.Column(
+ "heure_fin", postgresql.TIME(), autoincrement=False, nullable=True
+ )
+ )
+ batch_op.add_column(
+ sa.Column("jour", sa.DATE(), autoincrement=False, nullable=True)
+ )
+ op.execute("UPDATE notes_evaluation SET jour = DATE(date_debut);")
+ op.execute("UPDATE notes_evaluation SET heure_debut = date_debut::time;")
+ op.execute("UPDATE notes_evaluation SET heure_fin = date_fin::time;")
+ with op.batch_alter_table("notes_evaluation", schema=None) as batch_op:
+ batch_op.drop_column("date_fin")
+ batch_op.drop_column("date_debut")
diff --git a/scodoc.py b/scodoc.py
index aef11ae0a..5410dbdad 100755
--- a/scodoc.py
+++ b/scodoc.py
@@ -5,7 +5,7 @@
"""
-
+import datetime
from pprint import pprint as pp
import re
import sys
@@ -82,6 +82,7 @@ def make_shell_context():
"ctx": app.test_request_context(),
"current_app": flask.current_app,
"current_user": current_user,
+ "datetime": datetime,
"Departement": Departement,
"db": db,
"Evaluation": Evaluation,
diff --git a/tests/api/exemple-api-basic.py b/tests/api/exemple-api-basic.py
index eb7ae3b3a..5433fadcf 100644
--- a/tests/api/exemple-api-basic.py
+++ b/tests/api/exemple-api-basic.py
@@ -315,13 +315,12 @@ pp(GET(f"/formsemestre/880/resultats", headers=HEADERS)[0])
# jour = sem["date_fin"]
# evaluation_id = POST(
# s,
-# "/Notes/do_evaluation_create",
+# f"/moduleimpl/{mod['moduleimpl_id']}/evaluation/create",
# data={
-# "moduleimpl_id": mod["moduleimpl_id"],
# "coefficient": 1,
-# "jour": jour, # "5/9/2019",
-# "heure_debut": "9h00",
-# "heure_fin": "10h00",
+# "jour": jour, # "2023-08-23",
+# "heure_debut": "9:00",
+# "heure_fin": "10:00",
# "note_max": 20, # notes sur 20
# "description": "essai",
# },
diff --git a/tests/api/exemple-api-scodoc7.py b/tests/api/exemple-api-scodoc7.py
index 02e4cfb0f..fc80d13e5 100644
--- a/tests/api/exemple-api-scodoc7.py
+++ b/tests/api/exemple-api-scodoc7.py
@@ -165,37 +165,3 @@ assert isinstance(json.loads(r.text)[0]["billet_id"], int)
# print(f"{len(inscrits)} inscrits dans ce module")
# # prend le premier inscrit, au hasard:
# etudid = inscrits[0]["etudid"]
-
-# # ---- Création d'une evaluation le dernier jour du semestre
-# jour = sem["date_fin"]
-# evaluation_id = POST(
-# "/Notes/do_evaluation_create",
-# data={
-# "moduleimpl_id": mod["moduleimpl_id"],
-# "coefficient": 1,
-# "jour": jour, # "5/9/2019",
-# "heure_debut": "9h00",
-# "heure_fin": "10h00",
-# "note_max": 20, # notes sur 20
-# "description": "essai",
-# },
-# errmsg="échec création évaluation",
-# )
-
-# print(
-# f"Evaluation créée dans le module {mod['moduleimpl_id']}, evaluation_id={evaluation_id}"
-# )
-# print(
-# f"Pour vérifier, aller sur: {DEPT_URL}/Notes/moduleimpl_status?moduleimpl_id={mod['moduleimpl_id']}",
-# )
-
-# # ---- Saisie d'une note
-# junk = POST(
-# "/Notes/save_note",
-# data={
-# "etudid": etudid,
-# "evaluation_id": evaluation_id,
-# "value": 16.66, # la note !
-# "comment": "test API",
-# },
-# )
diff --git a/tests/api/test_api_evaluations.py b/tests/api/test_api_evaluations.py
index 9e1de1e23..e069cf4e8 100644
--- a/tests/api/test_api_evaluations.py
+++ b/tests/api/test_api_evaluations.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-"""Test Logos
+"""Test APi evaluations
Utilisation :
créer les variables d'environnement: (indiquer les valeurs
@@ -20,7 +20,13 @@ Utilisation :
import requests
from app.scodoc import sco_utils as scu
-from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers
+from tests.api.setup_test_api import (
+ API_URL,
+ CHECK_CERTIFICATE,
+ POST_JSON,
+ api_admin_headers,
+ api_headers,
+)
from tests.api.tools_test_api import (
verify_fields,
EVALUATIONS_FIELDS,
@@ -43,25 +49,25 @@ def test_evaluations(api_headers):
timeout=scu.SCO_TEST_API_TIMEOUT,
)
assert r.status_code == 200
- list_eval = r.json()
- assert list_eval
- assert isinstance(list_eval, list)
- for eval in list_eval:
- assert verify_fields(eval, EVALUATIONS_FIELDS) is True
- assert isinstance(eval["id"], int)
- assert isinstance(eval["note_max"], float)
- assert isinstance(eval["visi_bulletin"], bool)
- assert isinstance(eval["evaluation_type"], int)
- assert isinstance(eval["moduleimpl_id"], int)
- assert eval["description"] is None or isinstance(eval["description"], str)
- assert isinstance(eval["coefficient"], float)
- assert isinstance(eval["publish_incomplete"], bool)
- assert isinstance(eval["numero"], int)
- assert eval["date_debut"] is None or isinstance(eval["date_debut"], str)
- assert eval["date_fin"] is None or isinstance(eval["date_fin"], str)
- assert isinstance(eval["poids"], dict)
-
- assert eval["moduleimpl_id"] == moduleimpl_id
+ evaluations = r.json()
+ assert evaluations
+ assert isinstance(evaluations, list)
+ for e in evaluations:
+ assert verify_fields(e, EVALUATIONS_FIELDS)
+ assert isinstance(e["id"], int)
+ assert isinstance(e["note_max"], float)
+ assert isinstance(e["visi_bulletin"], bool)
+ assert isinstance(e["evaluation_type"], int)
+ assert isinstance(e["moduleimpl_id"], int)
+ assert e["description"] is None or isinstance(e["description"], str)
+ assert isinstance(e["coefficient"], float)
+ assert isinstance(e["publish_incomplete"], bool)
+ assert isinstance(e["numero"], int)
+ assert e["date_debut"] is None or isinstance(e["date_debut"], str)
+ assert e["date_fin"] is None or isinstance(e["date_fin"], str)
+ assert isinstance(e["poids"], dict)
+
+ assert e["moduleimpl_id"] == moduleimpl_id
def test_evaluation_notes(api_headers):
@@ -92,3 +98,31 @@ def test_evaluation_notes(api_headers):
assert isinstance(note["uid"], int)
assert eval_id == note["evaluation_id"]
+
+
+def test_evaluation_create(api_admin_headers):
+ """
+ Test /moduleimpl/<int:moduleimpl_id>/evaluation/create
+ """
+ moduleimpl_id = 20
+ e = POST_JSON(
+ f"/moduleimpl/{moduleimpl_id}/evaluation/create",
+ {"description": "eval test"},
+ api_admin_headers,
+ )
+ assert isinstance(e, dict)
+ assert verify_fields(e, EVALUATIONS_FIELDS)
+ # Check default values
+ assert e["note_max"] == 20.0
+ assert e["evaluation_type"] == 0
+ assert e["jour"] is None
+ assert e["date_debut"] is None
+ assert e["date_fin"] is None
+ assert e["visibulletin"] is True
+ assert e["publish_incomplete"] is False
+ assert e["coefficient"] == 1.0
+
+
+# TODO
+# - tester creation UE externe
+# - tester création base test et test API
diff --git a/tests/unit/sco_fake_gen.py b/tests/unit/sco_fake_gen.py
index 93508c7b3..37ca78cc9 100644
--- a/tests/unit/sco_fake_gen.py
+++ b/tests/unit/sco_fake_gen.py
@@ -16,7 +16,14 @@ import typing
from app import db, log
from app.auth.models import User
-from app.models import Departement, Formation, FormationModalite, Matiere
+from app.models import (
+ Departement,
+ Evaluation,
+ Formation,
+ FormationModalite,
+ Matiere,
+ ModuleImpl,
+)
from app.scodoc import notesdb as ndb
from app.scodoc import codes_cursus
from app.scodoc import sco_edit_matiere
@@ -307,14 +314,15 @@ class ScoFake(object):
publish_incomplete=None,
evaluation_type=None,
numero=None,
- ):
+ ) -> int:
args = locals()
del args["self"]
- oid = sco_evaluation_db.do_evaluation_create(**args)
- oids = sco_evaluation_db.do_evaluation_list(args={"evaluation_id": oid})
- if not oids:
- raise ScoValueError("evaluation not created !")
- return oids[0]
+ moduleimpl: ModuleImpl = db.session.get(ModuleImpl, moduleimpl_id)
+ assert moduleimpl
+ evaluation: Evaluation = Evaluation.create(moduleimpl=moduleimpl, **args)
+ db.session.add(evaluation)
+ db.session.commit()
+ return evaluation.id
@logging_meth
def create_note(
diff --git a/tools/fakedatabase/create_test_api_database.py b/tools/fakedatabase/create_test_api_database.py
index 3407afa24..55fbd4d4b 100644
--- a/tools/fakedatabase/create_test_api_database.py
+++ b/tools/fakedatabase/create_test_api_database.py
@@ -11,7 +11,6 @@ import datetime
import os
import random
import shutil
-import time
import sys
from app import db
@@ -23,6 +22,7 @@ from app.models import (
Absence,
Assiduite,
Departement,
+ Evaluation,
Formation,
FormSemestre,
FormSemestreEtape,
@@ -235,14 +235,13 @@ def inscrit_etudiants(etuds: list, formsemestre: FormSemestre):
def create_evaluations(formsemestre: FormSemestre):
- "creation d'une evaluation dans cahque modimpl du semestre"
- for modimpl in formsemestre.modimpls:
+ "Création d'une evaluation dans chaque modimpl du semestre"
+ for moduleimpl in formsemestre.modimpls:
args = {
- "moduleimpl_id": modimpl.id,
- "jour": datetime.date(2022, 3, 1) + datetime.timedelta(days=modimpl.id),
+ "jour": datetime.date(2022, 3, 1) + datetime.timedelta(days=moduleimpl.id),
"heure_debut": "8h00",
"heure_fin": "9h00",
- "description": f"Evaluation-{modimpl.module.code}",
+ "description": f"Evaluation-{moduleimpl.module.code}",
"note_max": 20,
"coefficient": 1.0,
"visibulletin": True,
@@ -250,7 +249,9 @@ def create_evaluations(formsemestre: FormSemestre):
"evaluation_type": None,
"numero": None,
}
- evaluation_id = sco_evaluation_db.do_evaluation_create(**args)
+ evaluation = Evaluation.create(moduleimpl=moduleimpl, **args)
+ db.session.add(evaluation)
+ db.session.commit()
def saisie_notes_evaluations(formsemestre: FormSemestre, user: User):
diff --git a/tools/test_api.sh b/tools/test_api.sh
index f8591db64..637d98d96 100755
--- a/tools/test_api.sh
+++ b/tools/test_api.sh
@@ -39,7 +39,14 @@ echo
echo Server PID "$pid" running on port "$PORT"
# ------------------
-pytest tests/api
+if [ "$#" -eq 1 ]
+then
+ echo "Starting pytest tests/api"
+ pytest tests/api
+else
+ echo "Starting pytest $@"
+ pytest "$@"
+fi
# ------------------
echo "Killing server"
--
GitLab