forked from ScoDoc/DocScoDoc
483 lines
17 KiB
Python
483 lines
17 KiB
Python
|
# -*- mode: python -*-
|
||
|
# -*- coding: utf-8 -*-
|
||
|
|
||
|
##############################################################################
|
||
|
#
|
||
|
# Gestion scolarite IUT
|
||
|
#
|
||
|
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||
|
#
|
||
|
# This program is free software; you can redistribute it and/or modify
|
||
|
# it under the terms of the GNU General Public License as published by
|
||
|
# the Free Software Foundation; either version 2 of the License, or
|
||
|
# (at your option) any later version.
|
||
|
#
|
||
|
# This program is distributed in the hope that it will be useful,
|
||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
# GNU General Public License for more details.
|
||
|
#
|
||
|
# You should have received a copy of the GNU General Public License
|
||
|
# along with this program; if not, write to the Free Software
|
||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||
|
#
|
||
|
# Emmanuel Viennet emmanuel.viennet@gmail.com
|
||
|
#
|
||
|
##############################################################################
|
||
|
|
||
|
"""Gestion evaluations (ScoDoc7, sans SQlAlchemy)
|
||
|
"""
|
||
|
|
||
|
import datetime
|
||
|
import pprint
|
||
|
|
||
|
import flask
|
||
|
from flask import url_for, g
|
||
|
from flask_login import current_user
|
||
|
|
||
|
from app import log
|
||
|
import app.scodoc.sco_utils as scu
|
||
|
import app.scodoc.notesdb as ndb
|
||
|
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||
|
|
||
|
from app.scodoc import sco_cache
|
||
|
from app.scodoc import sco_edit_module
|
||
|
from app.scodoc import sco_formsemestre
|
||
|
from app.scodoc import sco_moduleimpl
|
||
|
from app.scodoc import sco_news
|
||
|
from app.scodoc import sco_permissions_check
|
||
|
|
||
|
|
||
|
_evaluationEditor = ndb.EditableTable(
|
||
|
"notes_evaluation",
|
||
|
"evaluation_id",
|
||
|
(
|
||
|
"evaluation_id",
|
||
|
"moduleimpl_id",
|
||
|
"jour",
|
||
|
"heure_debut",
|
||
|
"heure_fin",
|
||
|
"description",
|
||
|
"note_max",
|
||
|
"coefficient",
|
||
|
"visibulletin",
|
||
|
"publish_incomplete",
|
||
|
"evaluation_type",
|
||
|
"numero",
|
||
|
),
|
||
|
sortkey="numero desc, jour desc, heure_debut desc", # plus recente d'abord
|
||
|
output_formators={
|
||
|
"jour": ndb.DateISOtoDMY,
|
||
|
"numero": ndb.int_null_is_zero,
|
||
|
},
|
||
|
input_formators={
|
||
|
"jour": ndb.DateDMYtoISO,
|
||
|
"heure_debut": ndb.TimetoISO8601, # converti par evaluation_enrich_dict
|
||
|
"heure_fin": ndb.TimetoISO8601, # converti par evaluation_enrich_dict
|
||
|
"visibulletin": bool,
|
||
|
"publish_incomplete": bool,
|
||
|
"evaluation_type": int,
|
||
|
},
|
||
|
)
|
||
|
|
||
|
|
||
|
def evaluation_enrich_dict(e):
|
||
|
"""add or convert some fileds 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["jouriso"] = ndb.DateDMYtoISO(e["jour"])
|
||
|
heure_debut, heure_fin = e["heure_debut"], e["heure_fin"]
|
||
|
d = ndb.TimeDuration(heure_debut, heure_fin)
|
||
|
if d is not None:
|
||
|
m = d % 60
|
||
|
e["duree"] = "%dh" % (d / 60)
|
||
|
if m != 0:
|
||
|
e["duree"] += "%02d" % m
|
||
|
else:
|
||
|
e["duree"] = ""
|
||
|
if heure_debut and (not heure_fin or heure_fin == heure_debut):
|
||
|
e["descrheure"] = " à " + heure_debut
|
||
|
elif heure_debut and heure_fin:
|
||
|
e["descrheure"] = " de %s à %s" % (heure_debut, heure_fin)
|
||
|
else:
|
||
|
e["descrheure"] = ""
|
||
|
# matin, apresmidi: utile pour se referer aux absences:
|
||
|
if heure_debut_dt < datetime.time(12, 00):
|
||
|
e["matin"] = 1
|
||
|
else:
|
||
|
e["matin"] = 0
|
||
|
if heure_fin_dt > datetime.time(12, 00):
|
||
|
e["apresmidi"] = 1
|
||
|
else:
|
||
|
e["apresmidi"] = 0
|
||
|
return e
|
||
|
|
||
|
|
||
|
def do_evaluation_list(args, sortkey=None):
|
||
|
"""List evaluations, sorted by numero (or most recent date first).
|
||
|
|
||
|
Ajoute les champs:
|
||
|
'duree' : '2h30'
|
||
|
'matin' : 1 (commence avant 12:00) ou 0
|
||
|
'apresmidi' : 1 (termine après 12:00) ou 0
|
||
|
'descrheure' : ' de 15h00 à 16h30'
|
||
|
"""
|
||
|
# Attention: transformation fonction ScoDc7 en SQLAlchemy
|
||
|
cnx = ndb.GetDBConnexion()
|
||
|
evals = _evaluationEditor.list(cnx, args, sortkey=sortkey)
|
||
|
# calcule duree (chaine de car.) de chaque evaluation et ajoute jouriso, matin, apresmidi
|
||
|
for e in evals:
|
||
|
evaluation_enrich_dict(e)
|
||
|
|
||
|
return evals
|
||
|
|
||
|
|
||
|
def do_evaluation_list_in_formsemestre(formsemestre_id):
|
||
|
"list evaluations in this formsemestre"
|
||
|
mods = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
|
||
|
evals = []
|
||
|
for modimpl in mods:
|
||
|
evals += do_evaluation_list(args={"moduleimpl_id": modimpl["moduleimpl_id"]})
|
||
|
return evals
|
||
|
|
||
|
|
||
|
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")
|
||
|
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")
|
||
|
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:
|
||
|
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
||
|
sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
|
||
|
d, m, y = [int(x) for x in sem["date_debut"].split("/")]
|
||
|
date_debut = datetime.date(y, m, d)
|
||
|
d, m, y = [int(x) for x in sem["date_fin"].split("/")]
|
||
|
date_fin = datetime.date(y, m, d)
|
||
|
# passe par ndb.DateDMYtoISO pour avoir date pivot
|
||
|
y, m, d = [int(x) for x in ndb.DateDMYtoISO(jour).split("-")]
|
||
|
jour = datetime.date(y, m, d)
|
||
|
if (jour > date_fin) or (jour < date_debut):
|
||
|
raise ScoValueError(
|
||
|
"La date de l'évaluation (%s/%s/%s) n'est pas dans le semestre !"
|
||
|
% (d, m, y)
|
||
|
)
|
||
|
heure_debut = args.get("heure_debut", None)
|
||
|
args["heure_debut"] = heure_debut
|
||
|
heure_fin = args.get("heure_fin", None)
|
||
|
args["heure_fin"] = 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 !")
|
||
|
|
||
|
|
||
|
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(
|
||
|
"Modification évaluation impossible pour %s" % current_user.get_nomplogin()
|
||
|
)
|
||
|
args = locals()
|
||
|
log("do_evaluation_create: args=" + str(args))
|
||
|
_check_evaluation_args(args)
|
||
|
# Check numeros
|
||
|
module_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 = module_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
|
||
|
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
||
|
mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
||
|
mod["moduleimpl_id"] = M["moduleimpl_id"]
|
||
|
mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
|
||
|
sco_news.add(
|
||
|
typ=sco_news.NEWS_NOTE,
|
||
|
object=moduleimpl_id,
|
||
|
text='Création d\'une évaluation dans <a href="%(url)s">%(titre)s</a>' % mod,
|
||
|
url=mod["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:
|
||
|
raise ValueError("evaluation inexistante !")
|
||
|
moduleimpl_id = the_evals[0]["moduleimpl_id"]
|
||
|
if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id):
|
||
|
raise AccessDenied(
|
||
|
"Modification évaluation impossible pour %s" % current_user.get_nomplogin()
|
||
|
)
|
||
|
args["moduleimpl_id"] = moduleimpl_id
|
||
|
_check_evaluation_args(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"])
|
||
|
|
||
|
|
||
|
def do_evaluation_delete(evaluation_id):
|
||
|
"delete evaluation"
|
||
|
the_evals = do_evaluation_list({"evaluation_id": evaluation_id})
|
||
|
if not the_evals:
|
||
|
raise ValueError("evaluation inexistante !")
|
||
|
moduleimpl_id = the_evals[0]["moduleimpl_id"]
|
||
|
if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id):
|
||
|
raise AccessDenied(
|
||
|
"Modification évaluation impossible pour %s" % current_user.get_nomplogin()
|
||
|
)
|
||
|
NotesDB = do_evaluation_get_all_notes(evaluation_id) # { etudid : value }
|
||
|
notes = [x["value"] for x in NotesDB.values()]
|
||
|
if notes:
|
||
|
raise ScoValueError(
|
||
|
"Impossible de supprimer cette évaluation: il reste des notes"
|
||
|
)
|
||
|
|
||
|
cnx = ndb.GetDBConnexion()
|
||
|
|
||
|
_evaluationEditor.delete(cnx, evaluation_id)
|
||
|
# inval cache pour ce semestre
|
||
|
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
||
|
sco_cache.invalidate_formsemestre(formsemestre_id=M["formsemestre_id"])
|
||
|
# news
|
||
|
|
||
|
mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
||
|
mod["moduleimpl_id"] = M["moduleimpl_id"]
|
||
|
mod["url"] = (
|
||
|
scu.NotesURL() + "/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
|
||
|
)
|
||
|
sco_news.add(
|
||
|
typ=sco_news.NEWS_NOTE,
|
||
|
object=moduleimpl_id,
|
||
|
text='Suppression d\'une évaluation dans <a href="%(url)s">%(titre)s</a>' % mod,
|
||
|
url=mod["url"],
|
||
|
)
|
||
|
|
||
|
|
||
|
# ancien _notes_getall
|
||
|
def do_evaluation_get_all_notes(
|
||
|
evaluation_id, table="notes_notes", filter_suppressed=True, by_uid=None
|
||
|
):
|
||
|
"""Toutes les notes pour une evaluation: { etudid : { 'value' : value, 'date' : date ... }}
|
||
|
Attention: inclut aussi les notes des étudiants qui ne sont plus inscrits au module.
|
||
|
"""
|
||
|
do_cache = (
|
||
|
filter_suppressed and table == "notes_notes" and (by_uid is None)
|
||
|
) # pas de cache pour (rares) appels via undo_notes ou specifiant un enseignant
|
||
|
if do_cache:
|
||
|
r = sco_cache.EvaluationCache.get(evaluation_id)
|
||
|
if r != None:
|
||
|
return r
|
||
|
cnx = ndb.GetDBConnexion()
|
||
|
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||
|
cond = " where evaluation_id=%(evaluation_id)s"
|
||
|
if by_uid:
|
||
|
cond += " and uid=%(by_uid)s"
|
||
|
|
||
|
cursor.execute(
|
||
|
"select * from " + table + cond,
|
||
|
{"evaluation_id": evaluation_id, "by_uid": by_uid},
|
||
|
)
|
||
|
res = cursor.dictfetchall()
|
||
|
d = {}
|
||
|
if filter_suppressed:
|
||
|
for x in res:
|
||
|
if x["value"] != scu.NOTES_SUPPRESS:
|
||
|
d[x["etudid"]] = x
|
||
|
else:
|
||
|
for x in res:
|
||
|
d[x["etudid"]] = x
|
||
|
if do_cache:
|
||
|
status = sco_cache.EvaluationCache.set(evaluation_id, d)
|
||
|
if not status:
|
||
|
log(f"Warning: EvaluationCache.set: {evaluation_id}\t{status}")
|
||
|
return d
|
||
|
|
||
|
|
||
|
def module_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('module_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 module_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")
|
||
|
module_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 module_evaluation_move(evaluation_id, after=0, redirect=1):
|
||
|
"""Move before/after previous one (decrement/increment numero)
|
||
|
(published)
|
||
|
"""
|
||
|
e = do_evaluation_list(args={"evaluation_id": evaluation_id})[0]
|
||
|
redirect = int(redirect)
|
||
|
# access: can change eval ?
|
||
|
if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=e["moduleimpl_id"]):
|
||
|
raise AccessDenied(
|
||
|
"Modification évaluation impossible pour %s" % current_user.get_nomplogin()
|
||
|
)
|
||
|
|
||
|
module_evaluation_renumber(e["moduleimpl_id"], only_if_unumbered=True)
|
||
|
e = do_evaluation_list(args={"evaluation_id": evaluation_id})[0]
|
||
|
|
||
|
after = int(after) # 0: deplace avant, 1 deplace apres
|
||
|
if after not in (0, 1):
|
||
|
raise ValueError('invalid value for "after"')
|
||
|
mod_evals = do_evaluation_list({"moduleimpl_id": e["moduleimpl_id"]})
|
||
|
if len(mod_evals) > 1:
|
||
|
idx = [p["evaluation_id"] for p in mod_evals].index(evaluation_id)
|
||
|
neigh = None # object to swap with
|
||
|
if after == 0 and idx > 0:
|
||
|
neigh = mod_evals[idx - 1]
|
||
|
elif after == 1 and idx < len(mod_evals) - 1:
|
||
|
neigh = mod_evals[idx + 1]
|
||
|
if neigh: #
|
||
|
if neigh["numero"] == e["numero"]:
|
||
|
log("Warning: module_evaluation_move: forcing renumber")
|
||
|
module_evaluation_renumber(e["moduleimpl_id"], only_if_unumbered=False)
|
||
|
else:
|
||
|
# swap numero with neighbor
|
||
|
e["numero"], neigh["numero"] = neigh["numero"], e["numero"]
|
||
|
do_evaluation_edit(e)
|
||
|
do_evaluation_edit(neigh)
|
||
|
# redirect to moduleimpl page:
|
||
|
if redirect:
|
||
|
return flask.redirect(
|
||
|
url_for(
|
||
|
"notes.moduleimpl_status",
|
||
|
scodoc_dept=g.scodoc_dept,
|
||
|
moduleimpl_id=e["moduleimpl_id"],
|
||
|
)
|
||
|
)
|