Update opolka/ScoDoc from ScoDoc/ScoDoc #2

Merged
opolka merged 1272 commits from ScoDoc/ScoDoc:master into master 2024-05-27 09:11:04 +02:00
23 changed files with 603 additions and 408 deletions
Showing only changes of commit ca04f3d5cb - Show all commits

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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)

View File

@ -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),

View File

@ -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

View File

@ -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)

View File

@ -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:

View File

@ -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

View File

@ -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,
)

View File

@ -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

View File

@ -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,

View File

@ -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")

View File

@ -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,

View File

@ -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",
# },

View File

@ -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",
# },
# )

View File

@ -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/<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

View File

@ -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(

View File

@ -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):

View File

@ -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"