From ca04f3d5cb321ef567573dc97c1415fcee3bd2be Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet 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 | 74 ++-- tests/unit/sco_fake_gen.py | 22 +- .../fakedatabase/create_test_api_database.py | 15 +- tools/test_api.sh | 9 +- 23 files changed, 603 insertions(+), 408 deletions(-) create mode 100644 migrations/versions/5c44d0d215ca_evaluation_date.py diff --git a/app/api/evaluations.py b/app/api/evaluations.py index af1c40af39..7fd84c5395 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//evaluation/create", methods=["POST"]) +@api_web_bp.route("/moduleimpl//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//delete", methods=["POST"]) +@api_web_bp.route("/evaluation//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 3231cc88e4..b9261c4fb2 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 0bb1b9021a..faf932e6f5 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 987b51706b..deb4bc263e 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"""""" + @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 { + moduleimpl.module.titre or '(module sans titre)'}""", + 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 c4d7c3fe0b..e84fe82b39 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 4d9b6aa2ac..3b7dd26547 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 72661afc80..74bc291075 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 178b029641..ca32ec46ac 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 260481b998..f0af65eac0 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 { - modimpl.module.titre or '(module sans titre)'}""", - 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 ac9f6e2fa3..f40a30e945 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()}

Opération non autorisée

@@ -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 84e5766ae7..3f167cf029 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""" Trier par date """ if nb_evaluations > 0: diff --git a/app/scodoc/sco_permissions_check.py b/app/scodoc/sco_permissions_check.py index 3ff5d3a3df..495a1eb4b3 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 22471d3c48..07177a6621 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 6208c16e1b..25645cebd1 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 da80388a31..9f344575b1 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 0000000000..66ab3617c0 --- /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 aef11ae0ab..5410dbdad9 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 eb7ae3b3a9..5433fadcff 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 02e4cfb0fc..fc80d13e55 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 9e1de1e235..e069cf4e8e 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) + 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 eval["moduleimpl_id"] == moduleimpl_id + 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//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 93508c7b38..37ca78cc96 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 3407afa24e..55fbd4d4b9 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 f8591db640..637d98d962 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"