WIP: modernisation evaluations

This commit is contained in:
Emmanuel Viennet 2023-08-22 17:02:00 +02:00
parent 94db71280f
commit ca04f3d5cb
23 changed files with 603 additions and 408 deletions

View File

@ -7,17 +7,19 @@
""" """
ScoDoc 9 API : accès aux évaluations ScoDoc 9 API : accès aux évaluations
""" """
import datetime
from flask import g, request from flask import g, request
from flask_json import as_json from flask_json import as_json
from flask_login import login_required from flask_login import current_user, login_required
import app import app
from app import db
from app.api import api_bp as bp, api_web_bp from app.api import api_bp as bp, api_web_bp
from app.decorators import scodoc, permission_required from app.decorators import scodoc, permission_required
from app.models import Evaluation, ModuleImpl, FormSemestre 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 from app.scodoc.sco_permissions import Permission
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -181,3 +183,64 @@ def evaluation_set_notes(evaluation_id: int):
return sco_saisie_notes.save_notes( return sco_saisie_notes.save_notes(
evaluation, notes, comment=data.get("comment", "") 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) "coef": fmt_note(e.coefficient)
if e.evaluation_type == scu.EVALUATION_NORMALE if e.evaluation_type == scu.EVALUATION_NORMALE
else None, 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, "description": e.description,
"evaluation_type": e.evaluation_type, "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": { "note": {
"value": fmt_note( "value": fmt_note(
eval_notes[etud.id], eval_notes[etud.id],
@ -298,6 +297,12 @@ class BulletinBUT:
) )
if has_request_context() if has_request_context()
else "na", 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 return d

View File

@ -202,12 +202,11 @@ def bulletin_but_xml_compat(
if e.visibulletin or version == "long": if e.visibulletin or version == "long":
x_eval = Element( x_eval = Element(
"evaluation", "evaluation",
jour=e.jour.isoformat() if e.jour else "", date_debut=e.date_debut.isoformat()
heure_debut=e.heure_debut.isoformat() if e.date_debut
if e.heure_debut
else "", else "",
heure_fin=e.heure_fin.isoformat() date_fin=e.date_fin.isoformat()
if e.heure_debut if e.date_debut
else "", else "",
coefficient=str(e.coefficient), coefficient=str(e.coefficient),
# pas les poids en XML compat # pas les poids en XML compat
@ -215,6 +214,16 @@ def bulletin_but_xml_compat(
description=quote_xml_attr(e.description), description=quote_xml_attr(e.description),
# notes envoyées sur 20, ceci juste pour garder trace: # notes envoyées sur 20, ceci juste pour garder trace:
note_max_origin=str(e.note_max), 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) x_mod.append(x_eval)
try: try:

View File

@ -5,17 +5,28 @@
import datetime import datetime
from operator import attrgetter 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 import db
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.events import ScolarNews
from app.models.moduleimpls import ModuleImpl from app.models.moduleimpls import ModuleImpl
from app.models.notes import NotesNotes from app.models.notes import NotesNotes
from app.models.ues import UniteEns 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.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) DEFAULT_EVALUATION_TIME = datetime.time(8, 0)
VALID_EVALUATION_TYPES = {0, 1, 2}
class Evaluation(db.Model): class Evaluation(db.Model):
"""Evaluation (contrôle, examen, ...)""" """Evaluation (contrôle, examen, ...)"""
@ -27,9 +38,8 @@ class Evaluation(db.Model):
moduleimpl_id = db.Column( moduleimpl_id = db.Column(
db.Integer, db.ForeignKey("notes_moduleimpl.id"), index=True db.Integer, db.ForeignKey("notes_moduleimpl.id"), index=True
) )
jour = db.Column(db.Date) date_debut = db.Column(db.DateTime(timezone=True), nullable=True)
heure_debut = db.Column(db.Time) date_fin = db.Column(db.DateTime(timezone=True), nullable=True)
heure_fin = db.Column(db.Time)
description = db.Column(db.Text) description = db.Column(db.Text)
note_max = db.Column(db.Float) note_max = db.Column(db.Float)
coefficient = db.Column(db.Float) coefficient = db.Column(db.Float)
@ -50,47 +60,106 @@ class Evaluation(db.Model):
def __repr__(self): def __repr__(self):
return f"""<Evaluation {self.id} { 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 ''}">""" 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: def to_dict(self) -> dict:
"Représentation dict (riche, compat ScoDoc 7)" "Représentation dict (riche, compat ScoDoc 7)"
e = dict(self.__dict__) e = dict(self.__dict__)
e.pop("_sa_instance_state", None) e.pop("_sa_instance_state", None)
# ScoDoc7 output_formators # ScoDoc7 output_formators
e["evaluation_id"] = self.id e["evaluation_id"] = self.id
e["jour"] = e["jour"].strftime("%d/%m/%Y") if e["jour"] else "" e["date_debut"] = e.date_debut.isoformat() if e.date_debut else None
if self.jour is None: e["date_fin"] = e.date_debut.isoformat() if e.date_fin else 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["numero"] = ndb.int_null_is_zero(e["numero"]) e["numero"] = ndb.int_null_is_zero(e["numero"])
e["poids"] = self.get_ue_poids_dict() # { ue_id : poids } 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) return evaluation_enrich_dict(e)
def to_dict_api(self) -> dict: def to_dict_api(self) -> dict:
"Représentation dict pour API JSON" "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 { return {
"coefficient": self.coefficient, "coefficient": self.coefficient,
"date_debut": date_debut, "date_debut": self.date_debut.isoformat(),
"date_fin": date_fin, "date_fin": self.date_fin.isoformat(),
"description": self.description, "description": self.description,
"evaluation_type": self.evaluation_type, "evaluation_type": self.evaluation_type,
"id": self.id, "id": self.id,
@ -104,11 +173,49 @@ class Evaluation(db.Model):
def from_dict(self, data): def from_dict(self, data):
"""Set evaluation attributes from given dict values.""" """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(): for k in self.__dict__.keys():
if k != "_sa_instance_state" and k != "id" and k in data: if k != "_sa_instance_state" and k != "id" and k in data:
setattr(self, k, data[k]) 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: def descr_heure(self) -> str:
"Description de la plage horaire pour affichages" "Description de la plage horaire pour affichages"
if self.heure_debut and ( if self.heure_debut and (
@ -146,19 +253,19 @@ class Evaluation(db.Model):
return copy return copy
def is_matin(self) -> bool: def is_matin(self) -> bool:
"Evaluation ayant lieu le matin (faux si pas de date)" "Evaluation commençant le matin (faux si pas de date)"
heure_debut_dt = self.heure_debut or datetime.time(8, 00) if not self.date_debut:
# 8:00 au cas ou pas d'heure (note externe?) return False
return bool(self.jour) and heure_debut_dt < datetime.time(12, 00) return self.date_debut.time() < NOON
def is_apresmidi(self) -> bool: def is_apresmidi(self) -> bool:
"Evaluation ayant lieu l'après midi (faux si pas de date)" "Evaluation commençant l'après midi (faux si pas de date)"
heure_debut_dt = self.heure_debut or datetime.time(8, 00) if not self.date_debut:
# 8:00 au cas ou pas d'heure (note externe?) return False
return bool(self.jour) and heure_debut_dt >= datetime.time(12, 00) return self.date_debut.time() >= NOON
def set_default_poids(self) -> bool: 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. C'est à dire à 1 si le coef. module/UE est non nul, 0 sinon.
Les poids existants ne sont pas modifiés. Les poids existants ne sont pas modifiés.
Return True if (uncommited) modification, False otherwise. Return True if (uncommited) modification, False otherwise.
@ -278,15 +385,13 @@ class EvaluationUEPoids(db.Model):
def evaluation_enrich_dict(e: dict): def evaluation_enrich_dict(e: dict):
"""add or convert some fields in an evaluation dict""" """add or convert some fields in an evaluation dict"""
# For ScoDoc7 compat # For ScoDoc7 compat
heure_debut_dt = e["heure_debut"] or datetime.time( heure_debut_dt = e["date_debut"].time()
8, 00 heure_fin_dt = e["date_fin"].time()
) # au cas ou pas d'heure (note externe?) e["heure_debut"] = heure_debut_dt.strftime("%Hh%M")
heure_fin_dt = e["heure_fin"] or datetime.time(8, 00) e["heure_fin"] = heure_fin_dt.strftime("%Hh%M")
e["heure_debut"] = ndb.TimefromISO8601(e["heure_debut"]) e["jour_iso"] = e["date_debut"].isoformat() # XXX
e["heure_fin"] = ndb.TimefromISO8601(e["heure_fin"])
e["jour_iso"] = ndb.DateDMYtoISO(e["jour"])
heure_debut, heure_fin = e["heure_debut"], e["heure_fin"] 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: if d is not None:
m = d % 60 m = d % 60
e["duree"] = "%dh" % (d / 60) e["duree"] = "%dh" % (d / 60)
@ -313,49 +418,118 @@ def evaluation_enrich_dict(e: dict):
return e return e
def check_evaluation_args(args): def check_convert_evaluation_args(moduleimpl: ModuleImpl, data: dict):
"Check coefficient, dates and duration, raises exception if invalid" """Check coefficient, dates and duration, raises exception if invalid.
moduleimpl_id = args["moduleimpl_id"] Convert date and time strings to date and time objects.
# check bareme
note_max = args.get("note_max", None) Set required default value for unspecified fields.
if note_max is None: May raise ScoValueError.
raise ScoValueError("missing note_max") """
# --- 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: try:
note_max = float(note_max) note_max = float(note_max)
except ValueError: except ValueError:
raise ScoValueError("Invalid note_max value") raise ScoValueError("Invalid note_max value")
if note_max < 0: if note_max < 0:
raise ScoValueError("Invalid note_max value (must be positive or null)") raise ScoValueError("Invalid note_max value (must be positive or null)")
# check coefficient data["note_max"] = note_max
coef = args.get("coefficient", None) # --- coefficient
if coef is None: coef = data.get("coefficient", 1.0) or 1.0
raise ScoValueError("missing coefficient")
try: try:
coef = float(coef) coef = float(coef)
except ValueError: except ValueError:
raise ScoValueError("Invalid coefficient value") raise ScoValueError("Invalid coefficient value")
if coef < 0: if coef < 0:
raise ScoValueError("Invalid coefficient value (must be positive or null)") raise ScoValueError("Invalid coefficient value (must be positive or null)")
# check date data["coefficient"] = coef
jour = args.get("jour", None) # --- jour (date de l'évaluation)
args["jour"] = jour jour = data.get("jour", None)
if jour: if jour and not isinstance(jour, datetime.date):
modimpl = db.session.get(ModuleImpl, moduleimpl_id) if date_format == "dmy":
formsemestre = modimpl.formsemestre
y, m, d = [int(x) for x in ndb.DateDMYtoISO(jour).split("-")] y, m, d = [int(x) for x in ndb.DateDMYtoISO(jour).split("-")]
jour = datetime.date(y, m, d) 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): if (jour > formsemestre.date_fin) or (jour < formsemestre.date_debut):
raise ScoValueError( raise ScoValueError(
"La date de l'évaluation (%s/%s/%s) n'est pas dans le semestre !" f"""La date de l'évaluation ({jour.strftime("%d/%m/%Y")}) n'est pas dans le semestre !""",
% (d, m, y),
dest_url="javascript:history.back();", dest_url="javascript:history.back();",
) )
heure_debut = args.get("heure_debut", None) data["jour"] = jour
args["heure_debut"] = heure_debut # --- heures
heure_fin = args.get("heure_fin", None) heure_debut = data.get("heure_debut", None)
args["heure_fin"] = heure_fin 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)): if jour and ((not heure_debut) or (not heure_fin)):
raise ScoValueError("Les heures doivent être précisées") raise ScoValueError("Les heures doivent être précisées")
d = ndb.TimeDuration(heure_debut, heure_fin) if heure_debut and heure_fin:
if d and ((d < 0) or (d > 60 * 12)): 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 !") 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) d.pop("module", None)
return d 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: def can_change_ens_by(self, user: User, raise_exc=False) -> bool:
"""Check if user can modify module resp. """Check if user can modify module resp.
If raise_exc, raises exception (AccessDenied or ScoLockedSemError) if not. 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 # --- Misc Tools
def DateDMYtoISO(dmy: str, null_is_empty=False) -> str: def DateDMYtoISO(dmy: str, null_is_empty=False) -> str: # XXX deprecated
"convert date string from french format to ISO" """Convert date string from french format to ISO.
If null_is_empty (default false), returns "" if no input.
"""
if not dmy: if not dmy:
if null_is_empty: if null_is_empty:
return "" return ""
@ -506,7 +508,7 @@ def DateISOtoDMY(isodate):
return "%02d/%02d/%04d" % (day, month, year) 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)" "convert time string to ISO 8601 (allow 16:03, 16h03, 16)"
if isinstance(t, datetime.time): if isinstance(t, datetime.time):
return t.isoformat() return t.isoformat()
@ -518,7 +520,7 @@ def TimetoISO8601(t, null_is_empty=False):
return t return t
def TimefromISO8601(t): def TimefromISO8601(t) -> str:
"convert time string from ISO 8601 to our display format" "convert time string from ISO 8601 to our display format"
if not t: if not t:
return t return t
@ -532,19 +534,6 @@ def TimefromISO8601(t):
return fs[0] + "h" + fs[1] # discard seconds 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): def float_null_is_zero(x):
if x is None or x == "": if x is None or x == "":
return 0.0 return 0.0

View File

@ -37,7 +37,7 @@ from app import db, ScoDocJSONEncoder
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models import but_validations 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.etudiants import Identite
from app.models.formsemestre import FormSemestre from app.models.formsemestre import FormSemestre
@ -324,7 +324,7 @@ def formsemestre_bulletinetud_published_dict(
def _list_modimpls( def _list_modimpls(
nt: NotesTableCompat, nt: NotesTableCompat,
etudid: int, etudid: int,
modimpls: list[ModuleImpl], modimpls: list[dict],
prefs: SemPreferences, prefs: SemPreferences,
version: str, version: str,
) -> list[dict]: ) -> list[dict]:
@ -398,24 +398,29 @@ def _list_modimpls(
# Evaluations incomplètes ou futures: # Evaluations incomplètes ou futures:
complete_eval_ids = set([e["evaluation_id"] for e in evals]) complete_eval_ids = set([e["evaluation_id"] for e in evals])
if prefs["bul_show_all_evals"]: if prefs["bul_show_all_evals"]:
all_evals = sco_evaluation_db.do_evaluation_list( evaluations = Evaluation.query.filter_by(
args={"moduleimpl_id": modimpl["moduleimpl_id"]} moduleimpl_id=modimpl["moduleimpl_id"]
) ).order_by(Evaluation.date_debut)
all_evals.reverse() # plus ancienne d'abord # plus ancienne d'abord
for e in all_evals: for e in evaluations:
if e["evaluation_id"] not in complete_eval_ids: if e.id not in complete_eval_ids:
mod_dict["evaluation"].append( mod_dict["evaluation"].append(
dict( 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( heure_debut=ndb.TimetoISO8601(
e["heure_debut"], null_is_empty=True e["heure_debut"], null_is_empty=True
), ),
heure_fin=ndb.TimetoISO8601( heure_fin=ndb.TimetoISO8601(
e["heure_fin"], null_is_empty=True e["heure_fin"], null_is_empty=True
), ),
coefficient=e["coefficient"],
description=quote_xml_attr(e["description"]),
incomplete="1",
) )
) )
modules_dict.append(mod_dict) modules_dict.append(mod_dict)

View File

@ -50,11 +50,11 @@ import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app import log from app import log
from app.but.bulletin_but_xml_compat import bulletin_but_xml_compat 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.models.formsemestre import FormSemestre
from app.scodoc import sco_assiduites from app.scodoc import sco_assiduites
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
from app.scodoc import sco_edit_ue 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_formsemestre
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_photos from app.scodoc import sco_photos
@ -320,12 +320,11 @@ def make_xml_formsemestre_bulletinetud(
if sco_preferences.get_preference( if sco_preferences.get_preference(
"bul_show_all_evals", formsemestre_id "bul_show_all_evals", formsemestre_id
): ):
all_evals = sco_evaluation_db.do_evaluation_list( evaluations = Evaluation.query.filter_by(
args={"moduleimpl_id": modimpl["moduleimpl_id"]} moduleimpl_id=modimpl["moduleimpl_id"]
) ).order_by(Evaluation.date_debut)
all_evals.reverse() # plus ancienne d'abord for e in evaluations:
for e in all_evals: if e.id not in complete_eval_ids:
if e["evaluation_id"] not in complete_eval_ids:
x_eval = Element( x_eval = Element(
"evaluation", "evaluation",
jour=ndb.DateDMYtoISO(e["jour"], null_is_empty=True), 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 import db, log
from app.models import Evaluation, ModuleImpl, ScolarNews 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.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError 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 'apresmidi' : 1 (termine après 12:00) ou 0
'descrheure' : ' de 15h00 à 16h30' 'descrheure' : ' de 15h00 à 16h30'
""" """
# Attention: transformation fonction ScoDoc7 en SQLAlchemy
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
evals = _evaluationEditor.list(cnx, args, sortkey=sortkey) evals = _evaluationEditor.list(cnx, args, sortkey=sortkey)
# calcule duree (chaine de car.) de chaque evaluation et ajoute jour_iso, matin, apresmidi # 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 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): def do_evaluation_edit(args):
"edit an evaluation" "edit an evaluation"
evaluation_id = args["evaluation_id"] evaluation_id = args["evaluation_id"]
the_evals = do_evaluation_list({"evaluation_id": evaluation_id}) evaluation: Evaluation = db.session.get(Evaluation, evaluation_id)
if not the_evals: if evaluation is None:
raise ValueError("evaluation inexistante !") 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( 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 args["moduleimpl_id"] = evaluation.moduleimpl.id
check_evaluation_args(args) check_convert_evaluation_args(evaluation.moduleimpl, args)
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
_evaluationEditor.edit(cnx, args) _evaluationEditor.edit(cnx, args)
# inval cache pour ce semestre # inval cache pour ce semestre
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] sco_cache.invalidate_formsemestre(
sco_cache.invalidate_formsemestre(formsemestre_id=M["formsemestre_id"]) formsemestre_id=evaluation.moduleimpl.formsemestre_id
)
def do_evaluation_delete(evaluation_id): def do_evaluation_delete(evaluation_id):
"delete evaluation" "delete evaluation"
evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id) evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id)
modimpl: ModuleImpl = evaluation.moduleimpl 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( raise AccessDenied(
f"Modification évaluation impossible pour {current_user.get_nomplogin()}" f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
) )
@ -287,68 +204,6 @@ def do_evaluation_get_all_notes(
return d 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): def moduleimpl_evaluation_move(evaluation_id: int, after=0, redirect=1):
"""Move before/after previous one (decrement/increment numero) """Move before/after previous one (decrement/increment numero)
(published) (published)
@ -357,12 +212,13 @@ def moduleimpl_evaluation_move(evaluation_id: int, after=0, redirect=1):
moduleimpl_id = evaluation.moduleimpl_id moduleimpl_id = evaluation.moduleimpl_id
redirect = int(redirect) redirect = int(redirect)
# access: can change eval ? # 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( raise AccessDenied(
f"Modification évaluation impossible pour {current_user.get_nomplogin()}" f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
) )
Evaluation.moduleimpl_evaluation_renumber(
moduleimpl_evaluation_renumber(moduleimpl_id, only_if_unumbered=True) evaluation.moduleimpl, only_if_unumbered=True
)
e = do_evaluation_list(args={"evaluation_id": evaluation_id})[0] e = do_evaluation_list(args={"evaluation_id": evaluation_id})[0]
after = int(after) # 0: deplace avant, 1 deplace apres 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: #
if neigh["numero"] == e["numero"]: if neigh["numero"] == e["numero"]:
log("Warning: moduleimpl_evaluation_move: forcing renumber") log("Warning: moduleimpl_evaluation_move: forcing renumber")
moduleimpl_evaluation_renumber( Evaluation.moduleimpl_evaluation_renumber(
e["moduleimpl_id"], only_if_unumbered=False evaluation.moduleimpl, only_if_unumbered=False
) )
else: else:
# swap numero with neighbor # swap numero with neighbor

View File

@ -83,7 +83,7 @@ def evaluation_create_form(
can_edit_poids = not preferences["but_disable_edit_poids_evaluations"] can_edit_poids = not preferences["but_disable_edit_poids_evaluations"]
min_note_max = scu.NOTES_PRECISION # le plus petit bareme possible 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""" return f"""
{html_sco_header.sco_header()} {html_sco_header.sco_header()}
<h2>Opération non autorisée</h2> <h2>Opération non autorisée</h2>
@ -356,8 +356,11 @@ def evaluation_create_form(
if edit: if edit:
sco_evaluation_db.do_evaluation_edit(tf[2]) sco_evaluation_db.do_evaluation_edit(tf[2])
else: else:
# création d'une evaluation (via fonction ScoDoc7) # création d'une evaluation
evaluation_id = sco_evaluation_db.do_evaluation_create(**tf[2]) evaluation = Evaluation.create(moduleimpl=modimpl, **tf[2])
db.session.add(evaluation)
db.session.commit()
evaluation_id = evaluation.id
if is_apc: if is_apc:
# Set poids # Set poids
evaluation = db.session.get(Evaluation, evaluation_id) 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""" top_table_links += f"""
<a class="stdlink" style="margin-left:2em;" href="{ <a class="stdlink" style="margin-left:2em;" href="{
url_for("notes.moduleimpl_evaluation_renumber", url_for("notes.moduleimpl_evaluation_renumber",
scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id, scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
redirect=1)
}">Trier par date</a> }">Trier par date</a>
""" """
if nb_evaluations > 0: if nb_evaluations > 0:

View File

@ -54,34 +54,6 @@ def can_edit_notes(authuser, moduleimpl_id, allow_ens=True):
return 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): def can_suppress_annotation(annotation_id):
"""True if current user can suppress this annotation """True if current user can suppress this annotation
Seuls l'auteur de l'annotation et le chef de dept peuvent supprimer 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 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 html_sco_header
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
from app.scodoc import sco_edit_matiere 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 """Inscrit les étudiants au moduleimpl, crée au besoin une évaluation
et enregistre les notes. et enregistre les notes.
""" """
moduleimpl: ModuleImpl = db.session.get(ModuleImpl, moduleimpl_id)
log( log(
f"external_ue_inscrit_et_note(moduleimpl_id={moduleimpl_id}, notes_etuds={notes_etuds})" 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, formsemestre_id,
list(notes_etuds.keys()), list(notes_etuds.keys()),
) )
# Création d'une évaluation si il n'y en a pas déjà: # Création d'une évaluation si il n'y en a pas déjà:
mod_evals = sco_evaluation_db.do_evaluation_list( if moduleimpl.evaluations.count() > 0:
args={"moduleimpl_id": moduleimpl_id}
)
if len(mod_evals):
# met la note dans le première évaluation existante: # met la note dans le première évaluation existante:
evaluation_id = mod_evals[0]["evaluation_id"] evaluation: Evaluation = moduleimpl.evaluations.first()
else: else:
# crée une évaluation: # crée une évaluation:
evaluation_id = sco_evaluation_db.do_evaluation_create( evaluation: Evaluation = Evaluation.create(
moduleimpl_id=moduleimpl_id, moduleimpl=moduleimpl,
note_max=20.0, note_max=20.0,
coefficient=1.0, coefficient=1.0,
publish_incomplete=True, publish_incomplete=True,
@ -185,7 +182,7 @@ def external_ue_inscrit_et_note(
# Saisie des notes # Saisie des notes
_, _, _ = sco_saisie_notes.notes_add( _, _, _ = sco_saisie_notes.notes_add(
current_user, current_user,
evaluation_id, evaluation.id,
list(notes_etuds.items()), list(notes_etuds.items()),
do_it=True, 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 from app.scodoc import sco_xml
import sco_version 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 # le répertoire static, lié à chaque release pour éviter les problèmes de caches
STATIC_DIR = ( STATIC_DIR = (
os.environ.get("SCRIPT_NAME", "") + "/ScoDoc/static/links/" + sco_version.SCOVERSION 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 import jury, res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models import ( from app.models import (
Evaluation,
Formation, Formation,
ScolarFormSemestreValidation,
ScolarAutorisationInscription, ScolarAutorisationInscription,
ScolarNews, ScolarNews,
Scolog, Scolog,
@ -134,6 +134,7 @@ from app.scodoc import sco_lycee
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
from app.scodoc import sco_moduleimpl_inscriptions from app.scodoc import sco_moduleimpl_inscriptions
from app.scodoc import sco_moduleimpl_status 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_placement
from app.scodoc import sco_poursuite_dut from app.scodoc import sco_poursuite_dut
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
@ -378,11 +379,40 @@ sco_publish(
sco_evaluations.formsemestre_evaluations_delai_correction, sco_evaluations.formsemestre_evaluations_delai_correction,
Permission.ScoView, Permission.ScoView,
) )
sco_publish(
"/moduleimpl_evaluation_renumber",
sco_evaluation_db.moduleimpl_evaluation_renumber, @bp.route("/moduleimpl_evaluation_renumber", methods=["GET", "POST"])
Permission.ScoView, @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( sco_publish(
"/moduleimpl_evaluation_move", "/moduleimpl_evaluation_move",
sco_evaluation_db.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 from pprint import pprint as pp
import re import re
import sys import sys
@ -82,6 +82,7 @@ def make_shell_context():
"ctx": app.test_request_context(), "ctx": app.test_request_context(),
"current_app": flask.current_app, "current_app": flask.current_app,
"current_user": current_user, "current_user": current_user,
"datetime": datetime,
"Departement": Departement, "Departement": Departement,
"db": db, "db": db,
"Evaluation": Evaluation, "Evaluation": Evaluation,

View File

@ -315,13 +315,12 @@ pp(GET(f"/formsemestre/880/resultats", headers=HEADERS)[0])
# jour = sem["date_fin"] # jour = sem["date_fin"]
# evaluation_id = POST( # evaluation_id = POST(
# s, # s,
# "/Notes/do_evaluation_create", # f"/moduleimpl/{mod['moduleimpl_id']}/evaluation/create",
# data={ # data={
# "moduleimpl_id": mod["moduleimpl_id"],
# "coefficient": 1, # "coefficient": 1,
# "jour": jour, # "5/9/2019", # "jour": jour, # "2023-08-23",
# "heure_debut": "9h00", # "heure_debut": "9:00",
# "heure_fin": "10h00", # "heure_fin": "10:00",
# "note_max": 20, # notes sur 20 # "note_max": 20, # notes sur 20
# "description": "essai", # "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") # print(f"{len(inscrits)} inscrits dans ce module")
# # prend le premier inscrit, au hasard: # # prend le premier inscrit, au hasard:
# etudid = inscrits[0]["etudid"] # 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 -*- # -*- coding: utf-8 -*-
"""Test Logos """Test APi evaluations
Utilisation : Utilisation :
créer les variables d'environnement: (indiquer les valeurs créer les variables d'environnement: (indiquer les valeurs
@ -20,7 +20,13 @@ Utilisation :
import requests import requests
from app.scodoc import sco_utils as scu 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 ( from tests.api.tools_test_api import (
verify_fields, verify_fields,
EVALUATIONS_FIELDS, EVALUATIONS_FIELDS,
@ -43,25 +49,25 @@ def test_evaluations(api_headers):
timeout=scu.SCO_TEST_API_TIMEOUT, timeout=scu.SCO_TEST_API_TIMEOUT,
) )
assert r.status_code == 200 assert r.status_code == 200
list_eval = r.json() evaluations = r.json()
assert list_eval assert evaluations
assert isinstance(list_eval, list) assert isinstance(evaluations, list)
for eval in list_eval: for e in evaluations:
assert verify_fields(eval, EVALUATIONS_FIELDS) is True assert verify_fields(e, EVALUATIONS_FIELDS)
assert isinstance(eval["id"], int) assert isinstance(e["id"], int)
assert isinstance(eval["note_max"], float) assert isinstance(e["note_max"], float)
assert isinstance(eval["visi_bulletin"], bool) assert isinstance(e["visi_bulletin"], bool)
assert isinstance(eval["evaluation_type"], int) assert isinstance(e["evaluation_type"], int)
assert isinstance(eval["moduleimpl_id"], int) assert isinstance(e["moduleimpl_id"], int)
assert eval["description"] is None or isinstance(eval["description"], str) assert e["description"] is None or isinstance(e["description"], str)
assert isinstance(eval["coefficient"], float) assert isinstance(e["coefficient"], float)
assert isinstance(eval["publish_incomplete"], bool) assert isinstance(e["publish_incomplete"], bool)
assert isinstance(eval["numero"], int) assert isinstance(e["numero"], int)
assert eval["date_debut"] is None or isinstance(eval["date_debut"], str) assert e["date_debut"] is None or isinstance(e["date_debut"], str)
assert eval["date_fin"] is None or isinstance(eval["date_fin"], str) assert e["date_fin"] is None or isinstance(e["date_fin"], str)
assert isinstance(eval["poids"], dict) assert isinstance(e["poids"], dict)
assert eval["moduleimpl_id"] == moduleimpl_id assert e["moduleimpl_id"] == moduleimpl_id
def test_evaluation_notes(api_headers): def test_evaluation_notes(api_headers):
@ -92,3 +98,31 @@ def test_evaluation_notes(api_headers):
assert isinstance(note["uid"], int) assert isinstance(note["uid"], int)
assert eval_id == note["evaluation_id"] 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 import db, log
from app.auth.models import User 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 notesdb as ndb
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
from app.scodoc import sco_edit_matiere from app.scodoc import sco_edit_matiere
@ -307,14 +314,15 @@ class ScoFake(object):
publish_incomplete=None, publish_incomplete=None,
evaluation_type=None, evaluation_type=None,
numero=None, numero=None,
): ) -> int:
args = locals() args = locals()
del args["self"] del args["self"]
oid = sco_evaluation_db.do_evaluation_create(**args) moduleimpl: ModuleImpl = db.session.get(ModuleImpl, moduleimpl_id)
oids = sco_evaluation_db.do_evaluation_list(args={"evaluation_id": oid}) assert moduleimpl
if not oids: evaluation: Evaluation = Evaluation.create(moduleimpl=moduleimpl, **args)
raise ScoValueError("evaluation not created !") db.session.add(evaluation)
return oids[0] db.session.commit()
return evaluation.id
@logging_meth @logging_meth
def create_note( def create_note(

View File

@ -11,7 +11,6 @@ import datetime
import os import os
import random import random
import shutil import shutil
import time
import sys import sys
from app import db from app import db
@ -23,6 +22,7 @@ from app.models import (
Absence, Absence,
Assiduite, Assiduite,
Departement, Departement,
Evaluation,
Formation, Formation,
FormSemestre, FormSemestre,
FormSemestreEtape, FormSemestreEtape,
@ -235,14 +235,13 @@ def inscrit_etudiants(etuds: list, formsemestre: FormSemestre):
def create_evaluations(formsemestre: FormSemestre): def create_evaluations(formsemestre: FormSemestre):
"creation d'une evaluation dans cahque modimpl du semestre" "Création d'une evaluation dans chaque modimpl du semestre"
for modimpl in formsemestre.modimpls: for moduleimpl in formsemestre.modimpls:
args = { args = {
"moduleimpl_id": modimpl.id, "jour": datetime.date(2022, 3, 1) + datetime.timedelta(days=moduleimpl.id),
"jour": datetime.date(2022, 3, 1) + datetime.timedelta(days=modimpl.id),
"heure_debut": "8h00", "heure_debut": "8h00",
"heure_fin": "9h00", "heure_fin": "9h00",
"description": f"Evaluation-{modimpl.module.code}", "description": f"Evaluation-{moduleimpl.module.code}",
"note_max": 20, "note_max": 20,
"coefficient": 1.0, "coefficient": 1.0,
"visibulletin": True, "visibulletin": True,
@ -250,7 +249,9 @@ def create_evaluations(formsemestre: FormSemestre):
"evaluation_type": None, "evaluation_type": None,
"numero": 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): def saisie_notes_evaluations(formsemestre: FormSemestre, user: User):

View File

@ -39,7 +39,14 @@ echo
echo Server PID "$pid" running on port "$PORT" echo Server PID "$pid" running on port "$PORT"
# ------------------ # ------------------
if [ "$#" -eq 1 ]
then
echo "Starting pytest tests/api"
pytest tests/api pytest tests/api
else
echo "Starting pytest $@"
pytest "$@"
fi
# ------------------ # ------------------
echo "Killing server" echo "Killing server"