forked from ScoDoc/ScoDoc
WIP: modernisation evaluations
This commit is contained in:
parent
0963462e31
commit
4e9ac3d3e2
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
58
migrations/versions/5c44d0d215ca_evaluation_date.py
Normal file
58
migrations/versions/5c44d0d215ca_evaluation_date.py
Normal 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")
|
@ -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,
|
||||
|
@ -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",
|
||||
# },
|
||||
|
@ -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",
|
||||
# },
|
||||
# )
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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):
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user