DocScoDoc/sco_saisie_notes.py

1303 lines
48 KiB
Python
Raw Normal View History

2020-09-26 16:19:37 +02:00
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
2021-01-01 17:51:08 +01:00
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
2020-09-26 16:19:37 +02:00
#
# 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@viennet.net
#
##############################################################################
"""Saisie des notes
Formulaire revu en juillet 2016
"""
import time
2020-09-26 16:19:37 +02:00
import datetime
import psycopg2
2020-09-26 16:19:37 +02:00
import sco_utils as scu
2020-09-26 16:19:37 +02:00
from notes_log import log
from TrivialFormulator import TrivialFormulator, TF
from notesdb import ScoDocCursor, quote_dict, DateISOtoDMY, DateDMYtoISO
from sco_exceptions import (
InvalidNoteValue,
AccessDenied,
NoteProcessError,
ScoValueError,
)
from sco_permissions import ScoEditAllNotes
2020-09-26 16:19:37 +02:00
import sco_formsemestre
import sco_moduleimpl
2020-09-26 16:19:37 +02:00
import sco_groups
import sco_groups_view
import sco_evaluations
import sco_parcours_dut
2020-09-26 16:19:37 +02:00
import sco_undo_notes
import htmlutils
import sco_excel
import scolars
import sco_news
from sco_news import NEWS_INSCR, NEWS_NOTE, NEWS_FORM, NEWS_SEM, NEWS_MISC
def can_edit_notes(context, authuser, moduleimpl_id, allow_ens=True):
"""True if authuser can enter or edit notes in this module.
If allow_ens, grant access to all ens in this module
Si des décisions de jury ont déjà été saisies dans ce semestre,
seul le directeur des études peut saisir des notes (et il ne devrait pas).
"""
uid = str(authuser)
M = sco_moduleimpl.do_moduleimpl_list(context, moduleimpl_id=moduleimpl_id)[0]
sem = sco_formsemestre.get_formsemestre(context, M["formsemestre_id"])
if sem["etat"] != "1":
return False # semestre verrouillé
if sco_parcours_dut.formsemestre_has_decisions(context, sem["formsemestre_id"]):
# il y a des décisions de jury dans ce semestre !
return (
authuser.has_permission(ScoEditAllNotes, context)
or uid in sem["responsables"]
)
else:
if (
(not authuser.has_permission(ScoEditAllNotes, context))
and uid != M["responsable_id"]
and uid not in sem["responsables"]
):
# enseignant (chargé de TD) ?
if allow_ens:
for ens in M["ens"]:
if ens["ens_id"] == uid:
return True
return False
else:
return True
2020-09-26 16:19:37 +02:00
def convert_note_from_string(
note,
note_max,
note_min=scu.NOTES_MIN,
2020-09-26 16:19:37 +02:00
etudid=None,
absents=[],
tosuppress=[],
invalids=[],
):
"""converti une valeur (chaine saisie) vers une note numérique (float)
Les listes absents, tosuppress et invalids sont modifiées
"""
invalid = False
note_value = None
note = note.replace(",", ".")
if note[:3] == "ABS":
note_value = None
absents.append(etudid)
elif note[:3] == "NEU" or note[:3] == "EXC":
note_value = scu.NOTES_NEUTRALISE
2020-09-26 16:19:37 +02:00
elif note[:3] == "ATT":
note_value = scu.NOTES_ATTENTE
2020-09-26 16:19:37 +02:00
elif note[:3] == "SUP":
note_value = scu.NOTES_SUPPRESS
2020-09-26 16:19:37 +02:00
tosuppress.append(etudid)
else:
try:
note_value = float(note)
if (note_value < note_min) or (note_value > note_max):
raise ValueError
except:
invalids.append(etudid)
invalid = True
return note_value, invalid
def _displayNote(val):
"""Convert note from DB to viewable string.
Utilisé seulement pour I/O vers formulaires (sans perte de precision)
(Utiliser fmt_note pour les affichages)
"""
if val is None:
val = "ABS"
elif val == scu.NOTES_NEUTRALISE:
2020-09-26 16:19:37 +02:00
val = "EXC" # excuse, note neutralise
elif val == scu.NOTES_ATTENTE:
2020-09-26 16:19:37 +02:00
val = "ATT" # attente, note neutralise
elif val == scu.NOTES_SUPPRESS:
2020-09-26 16:19:37 +02:00
val = "SUPR"
else:
val = "%g" % val
return val
def _check_notes(notes, evaluation, mod):
"""notes is a list of tuples (etudid, value)
mod is the module (used to ckeck type, for malus)
returns list of valid notes (etudid, float value)
and 4 lists of etudid: invalids, withoutnotes, absents, tosuppress, existingjury
"""
note_max = evaluation["note_max"]
if mod["module_type"] == scu.MODULE_STANDARD:
note_min = scu.NOTES_MIN
elif mod["module_type"] == scu.MODULE_MALUS:
2020-09-26 16:19:37 +02:00
note_min = -20.0
else:
raise ValueError("Invalid module type") # bug
L = [] # liste (etudid, note) des notes ok (ou absent)
invalids = [] # etudid avec notes invalides
withoutnotes = [] # etudid sans notes (champs vides)
absents = [] # etudid absents
tosuppress = [] # etudids avec ancienne note à supprimer
for (etudid, note) in notes:
note = str(note).strip().upper()
if note[:3] == "DEM":
continue # skip !
if note:
value, invalid = convert_note_from_string(
note,
note_max,
note_min=note_min,
etudid=etudid,
absents=absents,
tosuppress=tosuppress,
invalids=invalids,
)
if not invalid:
L.append((etudid, value))
else:
withoutnotes.append(etudid)
return L, invalids, withoutnotes, absents, tosuppress
def do_evaluation_upload_xls(context, REQUEST):
"""
Soumission d'un fichier XLS (evaluation_id, notefile)
"""
authuser = REQUEST.AUTHENTICATED_USER
evaluation_id = REQUEST.form["evaluation_id"]
comment = REQUEST.form["comment"]
E = context.do_evaluation_list({"evaluation_id": evaluation_id})[0]
M = sco_moduleimpl.do_moduleimpl_withmodule_list(
context, moduleimpl_id=E["moduleimpl_id"]
)[0]
2020-09-26 16:19:37 +02:00
# Check access
# (admin, respformation, and responsable_id)
if not can_edit_notes(context, authuser, E["moduleimpl_id"]):
2020-09-26 16:19:37 +02:00
# XXX imaginer un redirect + msg erreur
raise AccessDenied("Modification des notes impossible pour %s" % authuser)
#
data = REQUEST.form["notefile"].read()
diag, lines = sco_excel.Excel_to_list(data)
try:
if not lines:
raise InvalidNoteValue()
# -- search eval code
n = len(lines)
i = 0
while i < n:
if not lines[i]:
diag.append("Erreur: format invalide (ligne vide ?)")
raise InvalidNoteValue()
f0 = lines[i][0].strip()
if f0 and f0[0] == "!":
break
i = i + 1
if i == n:
diag.append("Erreur: format invalide ! (pas de ligne evaluation_id)")
raise InvalidNoteValue()
eval_id = lines[i][0].strip()[1:]
if eval_id != evaluation_id:
diag.append(
"Erreur: fichier invalide: le code d'évaluation de correspond pas ! ('%s' != '%s')"
% (eval_id, evaluation_id)
)
raise InvalidNoteValue()
# --- get notes -> list (etudid, value)
# ignore toutes les lignes ne commençant pas par !
notes = []
ni = i + 1
try:
for line in lines[i + 1 :]:
if line:
cell0 = line[0].strip()
if cell0 and cell0[0] == "!":
etudid = cell0[1:]
if len(line) > 4:
val = line[4].strip()
else:
val = "" # ligne courte: cellule vide
if etudid:
notes.append((etudid, val))
ni += 1
except:
diag.append(
'Erreur: feuille invalide ! (erreur ligne %d)<br/>"%s"'
% (ni, str(lines[ni]))
)
raise InvalidNoteValue()
# -- check values
2021-02-03 22:00:41 +01:00
L, invalids, withoutnotes, absents, _ = _check_notes(notes, E, M["module"])
2020-09-26 16:19:37 +02:00
if len(invalids):
diag.append(
"Erreur: la feuille contient %d notes invalides</p>" % len(invalids)
)
if len(invalids) < 25:
etudsnames = [
context.getEtudInfo(etudid=etudid, filled=True)[0]["nomprenom"]
for etudid in invalids
]
diag.append("Notes invalides pour: " + ", ".join(etudsnames))
raise InvalidNoteValue()
else:
nb_changed, nb_suppress, existing_decisions = _notes_add(
context, authuser, evaluation_id, L, comment
)
# news
E = context.do_evaluation_list({"evaluation_id": evaluation_id})[0]
M = sco_moduleimpl.do_moduleimpl_list(
context, moduleimpl_id=E["moduleimpl_id"]
)[0]
2020-09-26 16:19:37 +02:00
mod = context.do_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(
context,
REQUEST,
typ=NEWS_NOTE,
object=M["moduleimpl_id"],
text='Chargement notes dans <a href="%(url)s">%(titre)s</a>' % mod,
url=mod["url"],
)
msg = (
"<p>%d notes changées (%d sans notes, %d absents, %d note supprimées)</p>"
% (nb_changed, len(withoutnotes), len(absents), nb_suppress)
)
if existing_decisions:
msg += """<p class="warning">Important: il y avait déjà des décisions de jury enregistrées, qui sont potentiellement à revoir suite à cette modification !</p>"""
# msg += '<p>' + str(notes) # debug
return 1, msg
except InvalidNoteValue:
if diag:
msg = (
'<ul class="tf-msg"><li class="tf_msg">'
+ '</li><li class="tf_msg">'.join(diag)
+ "</li></ul>"
)
else:
msg = '<ul class="tf-msg"><li class="tf_msg">Une erreur est survenue</li></ul>'
return 0, msg + "<p>(pas de notes modifiées)</p>"
def do_evaluation_set_missing(
context, evaluation_id, value, REQUEST=None, dialog_confirmed=False
):
2021-01-01 18:40:47 +01:00
"""Initialisation des notes manquantes"""
2020-09-26 16:19:37 +02:00
authuser = REQUEST.AUTHENTICATED_USER
evaluation_id = REQUEST.form["evaluation_id"]
E = context.do_evaluation_list({"evaluation_id": evaluation_id})[0]
M = sco_moduleimpl.do_moduleimpl_withmodule_list(
context, moduleimpl_id=E["moduleimpl_id"]
)[0]
2020-09-26 16:19:37 +02:00
# Check access
# (admin, respformation, and responsable_id)
if not can_edit_notes(context, authuser, E["moduleimpl_id"]):
2020-09-26 16:19:37 +02:00
# XXX imaginer un redirect + msg erreur
raise AccessDenied("Modification des notes impossible pour %s" % authuser)
#
NotesDB = context._notes_getall(evaluation_id)
etudids = sco_groups.do_evaluation_listeetuds_groups(
context, evaluation_id, getallstudents=True, include_dems=False
)
notes = []
for etudid in etudids: # pour tous les inscrits
if not NotesDB.has_key(etudid): # pas de note
notes.append((etudid, value))
# Check value
2021-02-03 22:00:41 +01:00
L, invalids, _, _, _ = _check_notes(notes, E, M["module"])
2020-09-26 16:19:37 +02:00
diag = ""
if len(invalids):
diag = "Valeur %s invalide" % value
if diag:
return (
context.sco_header(REQUEST)
+ '<h2>%s</h2><p><a href="saisie_notes?evaluation_id=%s">Recommencer</a>'
% (diag, evaluation_id)
+ context.sco_footer(REQUEST)
)
# Confirm action
if not dialog_confirmed:
return context.confirmDialog(
"""<h2>Mettre toutes les notes manquantes de l'évaluation
à la valeur %s ?</h2>
<p>Seuls les étudiants pour lesquels aucune note (ni valeur, ni ABS, ni EXC)
n'a été rentrée seront affectés.</p>
<p><b>%d étudiants concernés par ce changement de note.</b></p>
<p class="warning">Attention, les étudiants sans notes de tous les groupes de ce semestre seront affectés.</p>
"""
% (value, len(L)),
dest_url="",
REQUEST=REQUEST,
cancel_url="saisie_notes?evaluation_id=%s" % evaluation_id,
parameters={"evaluation_id": evaluation_id, "value": value},
)
# ok
comment = "Initialisation notes manquantes"
2021-02-03 22:00:41 +01:00
nb_changed, _, _ = _notes_add(context, authuser, evaluation_id, L, comment)
2020-09-26 16:19:37 +02:00
# news
M = sco_moduleimpl.do_moduleimpl_list(context, moduleimpl_id=E["moduleimpl_id"])[0]
2020-09-26 16:19:37 +02:00
mod = context.do_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(
context,
REQUEST,
typ=NEWS_NOTE,
object=M["moduleimpl_id"],
text='Initialisation notes dans <a href="%(url)s">%(titre)s</a>' % mod,
url=mod["url"],
)
return (
context.sco_header(REQUEST)
+ """<h2>%d notes changées</h2>
<ul>
<li><a class="stdlink" href="saisie_notes?evaluation_id=%s">
Revenir au formulaire de saisie des notes</a></li>
<li><a class="stdlink" href="moduleimpl_status?moduleimpl_id=%s">
Tableau de bord du module</a></li>
</ul>
"""
% (nb_changed, evaluation_id, M["moduleimpl_id"])
+ context.sco_footer(REQUEST)
)
def evaluation_suppress_alln(context, evaluation_id, REQUEST, dialog_confirmed=False):
"suppress all notes in this eval"
authuser = REQUEST.AUTHENTICATED_USER
E = context.do_evaluation_list({"evaluation_id": evaluation_id})[0]
if can_edit_notes(context, authuser, E["moduleimpl_id"], allow_ens=False):
2020-09-26 16:19:37 +02:00
# On a le droit de modifier toutes les notes
# recupere les etuds ayant une note
NotesDB = context._notes_getall(evaluation_id)
elif can_edit_notes(context, authuser, E["moduleimpl_id"], allow_ens=True):
2020-09-26 16:19:37 +02:00
# Enseignant associé au module: ne peut supprimer que les notes qu'il a saisi
NotesDB = context._notes_getall(evaluation_id, by_uid=str(authuser))
else:
raise AccessDenied("Modification des notes impossible pour %s" % authuser)
notes = [(etudid, scu.NOTES_SUPPRESS) for etudid in NotesDB.keys()]
2020-09-26 16:19:37 +02:00
if not dialog_confirmed:
nb_changed, nb_suppress, existing_decisions = _notes_add(
context, authuser, evaluation_id, notes, do_it=False
)
msg = "<p>Confirmer la suppression des %d notes ?</p>" % nb_suppress
if existing_decisions:
msg += """<p class="warning">Important: il y a déjà des décisions de jury enregistrées, qui seront potentiellement à revoir suite à cette modification !</p>"""
return context.confirmDialog(
msg,
dest_url="",
REQUEST=REQUEST,
OK="Supprimer les notes",
cancel_url="moduleimpl_status?moduleimpl_id=%s" % E["moduleimpl_id"],
parameters={"evaluation_id": evaluation_id},
)
# modif
nb_changed, nb_suppress, existing_decisions = _notes_add(
context, authuser, evaluation_id, notes, comment="effacer tout"
)
assert nb_changed == nb_suppress
H = ["<p>%s notes supprimées</p>" % nb_suppress]
if existing_decisions:
H.append(
"""<p class="warning">Important: il y avait déjà des décisions de jury enregistrées, qui sont potentiellement à revoir suite à cette modification !</p>"""
)
H += [
'<p><a class="stdlink" href="moduleimpl_status?moduleimpl_id=%s">continuer</a>'
% E["moduleimpl_id"]
]
# news
M = sco_moduleimpl.do_moduleimpl_list(context, moduleimpl_id=E["moduleimpl_id"])[0]
2020-09-26 16:19:37 +02:00
mod = context.do_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(
context,
REQUEST,
typ=NEWS_NOTE,
object=M["moduleimpl_id"],
text='Suppression des notes d\'une évaluation dans <a href="%(url)s">%(titre)s</a>'
% mod,
url=mod["url"],
)
return context.sco_header(REQUEST) + "\n".join(H) + context.sco_footer(REQUEST)
def _notes_add(context, uid, evaluation_id, notes, comment=None, do_it=True):
"""
Insert or update notes
notes is a list of tuples (etudid,value)
If do_it is False, simulate the process and returns the number of values that
WOULD be changed or suppressed.
Nota:
- si la note existe deja avec valeur distincte, ajoute une entree au log (notes_notes_log)
Return number of changed notes
"""
uid = str(uid)
now = apply(
psycopg2.Timestamp, time.localtime()[:6]
) # datetime.datetime.now().isoformat()
# Verifie inscription et valeur note
2021-02-03 22:00:41 +01:00
_ = {}.fromkeys(
2020-09-26 16:19:37 +02:00
sco_groups.do_evaluation_listeetuds_groups(
context, evaluation_id, getallstudents=True, include_dems=True
)
)
for (etudid, value) in notes:
if not ((value is None) or (type(value) == type(1.0))):
raise NoteProcessError(
"etudiant %s: valeur de note invalide (%s)" % (etudid, value)
)
# Recherche notes existantes
NotesDB = context._notes_getall(evaluation_id)
# Met a jour la base
cnx = context.GetDBConnexion(autocommit=False)
cursor = cnx.cursor(cursor_factory=ScoDocCursor)
nb_changed = 0
nb_suppress = 0
E = context.do_evaluation_list({"evaluation_id": evaluation_id})[0]
M = sco_moduleimpl.do_moduleimpl_list(context, moduleimpl_id=E["moduleimpl_id"])[0]
2020-09-26 16:19:37 +02:00
existing_decisions = (
[]
) # etudids pour lesquels il y a une decision de jury et que la note change
try:
for (etudid, value) in notes:
changed = False
if not NotesDB.has_key(etudid):
# nouvelle note
if value != scu.NOTES_SUPPRESS:
2020-09-26 16:19:37 +02:00
if do_it:
aa = {
"etudid": etudid,
"evaluation_id": evaluation_id,
"value": value,
"comment": comment,
"uid": uid,
"date": now,
}
quote_dict(aa)
cursor.execute(
"insert into notes_notes (etudid,evaluation_id,value,comment,date,uid) values (%(etudid)s,%(evaluation_id)s,%(value)s,%(comment)s,%(date)s,%(uid)s)",
aa,
)
changed = True
else:
# il y a deja une note
oldval = NotesDB[etudid]["value"]
if type(value) != type(oldval):
changed = True
elif type(value) == type(1.0) and (
abs(value - oldval) > scu.NOTES_PRECISION
2020-09-26 16:19:37 +02:00
):
changed = True
elif value != oldval:
changed = True
if changed:
# recopie l'ancienne note dans notes_notes_log, puis update
if do_it:
cursor.execute(
"insert into notes_notes_log (etudid,evaluation_id,value,comment,date,uid) select etudid,evaluation_id,value,comment,date,uid from notes_notes where etudid=%(etudid)s and evaluation_id=%(evaluation_id)s",
{"etudid": etudid, "evaluation_id": evaluation_id},
)
aa = {
"etudid": etudid,
"evaluation_id": evaluation_id,
"value": value,
"date": now,
"comment": comment,
"uid": uid,
}
quote_dict(aa)
if value != scu.NOTES_SUPPRESS:
2020-09-26 16:19:37 +02:00
if do_it:
cursor.execute(
"update notes_notes set value=%(value)s, comment=%(comment)s, date=%(date)s, uid=%(uid)s where etudid=%(etudid)s and evaluation_id=%(evaluation_id)s",
aa,
)
else: # suppression ancienne note
if do_it:
log(
"_notes_add, suppress, evaluation_id=%s, etudid=%s, oldval=%s"
% (evaluation_id, etudid, oldval)
)
cursor.execute(
"delete from notes_notes where etudid=%(etudid)s and evaluation_id=%(evaluation_id)s",
aa,
)
# garde trace de la suppression dans l'historique:
aa["value"] = scu.NOTES_SUPPRESS
2020-09-26 16:19:37 +02:00
cursor.execute(
"insert into notes_notes_log (etudid,evaluation_id,value,comment,date,uid) values (%(etudid)s, %(evaluation_id)s, %(value)s, %(comment)s, %(date)s, %(uid)s)",
aa,
)
nb_suppress += 1
if changed:
nb_changed += 1
if has_existing_decision(context, M, E, etudid):
existing_decisions.append(etudid)
except:
log("*** exception in _notes_add")
if do_it:
# inval cache
context._inval_cache(
formsemestre_id=M["formsemestre_id"]
) # > modif notes (exception)
cnx.rollback() # abort
raise # re-raise exception
if do_it:
cnx.commit()
context._inval_cache(formsemestre_id=M["formsemestre_id"]) # > modif notes
context.get_evaluations_cache().inval_cache(key=evaluation_id)
return nb_changed, nb_suppress, existing_decisions
def saisie_notes_tableur(context, evaluation_id, group_ids=[], REQUEST=None):
2021-01-01 18:40:47 +01:00
"""Saisie des notes via un fichier Excel"""
2020-09-26 16:19:37 +02:00
authuser = REQUEST.AUTHENTICATED_USER
2021-01-01 18:40:47 +01:00
authusername = str(authuser)
2020-09-26 16:19:37 +02:00
evals = context.do_evaluation_list({"evaluation_id": evaluation_id})
if not evals:
raise ScoValueError("invalid evaluation_id")
E = evals[0]
M = sco_moduleimpl.do_moduleimpl_list(context, moduleimpl_id=E["moduleimpl_id"])[0]
2020-09-26 16:19:37 +02:00
formsemestre_id = M["formsemestre_id"]
if not can_edit_notes(context, authuser, E["moduleimpl_id"]):
2020-09-26 16:19:37 +02:00
return (
context.sco_header(REQUEST)
+ "<h2>Modification des notes impossible pour %s</h2>" % authusername
+ """<p>(vérifiez que le semestre n'est pas verrouillé et que vous
avez l'autorisation d'effectuer cette opération)</p>
<p><a href="moduleimpl_status?moduleimpl_id=%s">Continuer</a></p>
"""
% E["moduleimpl_id"]
+ context.sco_footer(REQUEST)
)
if E["description"]:
page_title = 'Saisie des notes de "%s"' % E["description"]
else:
page_title = "Saisie des notes"
# Informations sur les groupes à afficher:
groups_infos = sco_groups_view.DisplayedGroupsInfos(
context,
group_ids=group_ids,
formsemestre_id=formsemestre_id,
select_all_when_unspecified=True,
etat=None,
REQUEST=REQUEST,
)
H = [
context.sco_header(
REQUEST,
page_title=page_title,
javascripts=sco_groups_view.JAVASCRIPTS,
cssstyles=sco_groups_view.CSSSTYLES,
init_qtip=True,
),
sco_evaluations.evaluation_describe(
context, evaluation_id=evaluation_id, REQUEST=REQUEST
),
"""<span class="eval_title">Saisie des notes par fichier</span>""",
]
# Menu choix groupe:
H.append("""<div id="group-tabs"><table><tr><td>""")
H.append(sco_groups_view.form_groups_choice(context, groups_infos))
H.append("</td></tr></table></div>")
H.append(
"""<div class="saisienote_etape1">
<span class="titredivsaisienote">Etape 1 : </span>
<ul>
<li><a href="feuille_saisie_notes?evaluation_id=%s&%s" class="stdlink" id="lnk_feuille_saisie">obtenir le fichier tableur à remplir</a></li>
<li>ou <a class="stdlink" href="saisie_notes?evaluation_id=%s">aller au formulaire de saisie</a></li>
</ul>
</div>
<form><input type="hidden" name="evaluation_id" id="formnotes_evaluation_id" value="%s"/></form>
"""
% (evaluation_id, groups_infos.groups_query_args, evaluation_id, evaluation_id)
)
H.append(
"""<div class="saisienote_etape2">
<span class="titredivsaisienote">Etape 2 : chargement d'un fichier de notes</span>""" #'
)
nf = TrivialFormulator(
REQUEST.URL0,
REQUEST.form,
(
("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}),
(
"notefile",
{"input_type": "file", "title": "Fichier de note (.xls)", "size": 44},
),
(
"comment",
{
"size": 44,
"title": "Commentaire",
"explanation": "(la colonne remarque du fichier excel est ignorée)",
},
),
),
formid="notesfile",
submitlabel="Télécharger",
)
if nf[0] == 0:
H.append(
"""<p>Le fichier doit être un fichier tableur obtenu via
l'étape 1 ci-dessus, puis complété et enregistré au format Excel.
</p>"""
)
H.append(nf[1])
elif nf[0] == -1:
H.append("<p>Annulation</p>")
elif nf[0] == 1:
updiag = do_evaluation_upload_xls(context, REQUEST)
if updiag[0]:
H.append(updiag[1])
H.append(
"""<p>Notes chargées.&nbsp;&nbsp;&nbsp;
<a class="stdlink" href="moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s">
Revenir au tableau de bord du module</a>
&nbsp;&nbsp;&nbsp;
<a class="stdlink" href="saisie_notes?evaluation_id=%(evaluation_id)s">Charger d'autres notes dans cette évaluation</a>
</p>"""
% E
)
else:
H.append("""<p class="redboldtext">Notes non chargées !</p>""" + updiag[1])
H.append(
"""
<p><a class="stdlink" href="saisie_notes_tableur?evaluation_id=%(evaluation_id)s">
Reprendre</a>
</p>"""
% E
)
#
H.append("""</div><h3>Autres opérations</h3><ul>""")
if can_edit_notes(
context, REQUEST.AUTHENTICATED_USER, E["moduleimpl_id"], allow_ens=False
2020-09-26 16:19:37 +02:00
):
H.append(
"""
<li>
<form action="do_evaluation_set_missing" method="get">
Mettre toutes les notes manquantes à <input type="text" size="5" name="value"/>
<input type="submit" value="OK"/>
<input type="hidden" name="evaluation_id" value="%s"/>
<em>ABS indique "absent" (zéro), EXC "excusé" (neutralisées), ATT "attente"</em>
</form>
</li>
<li><a class="stdlink" href="evaluation_suppress_alln?evaluation_id=%s">Effacer toutes les notes de cette évaluation</a> (ceci permet ensuite de supprimer l'évaluation si besoin)
</li>"""
% (evaluation_id, evaluation_id)
) #'
H.append(
"""<li><a class="stdlink" href="moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s">Revenir au module</a></li>
<li><a class="stdlink" href="saisie_notes?evaluation_id=%(evaluation_id)s">Revenir au formulaire de saisie</a></li>
</ul>"""
% E
)
H.append(
"""<h3>Explications</h3>
<ol>
<li>Etape 1:
<ol><li>choisir le ou les groupes d'étudiants;</li>
<li>télécharger le fichier Excel à remplir.</li>
</ol>
</li>
<li>Etape 2 (cadre vert): Indiquer le fichier Excel <em>téléchargé à l'étape 1</em> et dans lequel on a saisi des notes. Remarques:
<ul>
<li>le fichier Excel peut être incomplet: on peut ne saisir que quelques notes et répéter l'opération (en téléchargeant un nouveau fichier) plus tard;</li>
<li>seules les valeurs des notes modifiées sont prises en compte;</li>
<li>seules les notes sont extraites du fichier Excel;</li>
<li>on peut optionnellement ajouter un commentaire (type "copies corrigées par Dupont", ou "Modif. suite à contestation") dans la case "Commentaire".
</li>
<li>le fichier Excel <em>doit impérativement être celui chargé à l'étape 1 pour cette évaluation</em>. Il n'est pas possible d'utiliser une liste d'appel ou autre document Excel téléchargé d'une autre page.</li>
</ul>
</li>
</ol>
"""
)
H.append(context.sco_footer(REQUEST))
return "\n".join(H)
def feuille_saisie_notes(context, evaluation_id, group_ids=[], REQUEST=None):
2021-01-01 18:40:47 +01:00
"""Document Excel pour saisie notes dans l'évaluation et les groupes indiqués"""
2020-09-26 16:19:37 +02:00
evals = context.do_evaluation_list({"evaluation_id": evaluation_id})
if not evals:
raise ScoValueError("invalid evaluation_id")
E = evals[0]
M = sco_moduleimpl.do_moduleimpl_list(context, moduleimpl_id=E["moduleimpl_id"])[0]
2020-09-26 16:19:37 +02:00
formsemestre_id = M["formsemestre_id"]
Mod = context.do_module_list(args={"module_id": M["module_id"]})[0]
sem = sco_formsemestre.get_formsemestre(context, M["formsemestre_id"])
if E["jour"]:
indication_date = DateDMYtoISO(E["jour"])
else:
indication_date = scu.sanitize_filename(E["description"])[:12]
2020-09-26 16:19:37 +02:00
evalname = "%s-%s" % (Mod["code"], indication_date)
if E["description"]:
evaltitre = "%s du %s" % (E["description"], E["jour"])
else:
evaltitre = "évaluation du %s" % E["jour"]
description = "%s en %s (%s) resp. %s" % (
evaltitre,
Mod["abbrev"],
Mod["code"],
scu.strcapitalize(M["responsable_id"]),
2020-09-26 16:19:37 +02:00
)
groups_infos = sco_groups_view.DisplayedGroupsInfos(
context,
group_ids=group_ids,
formsemestre_id=formsemestre_id,
select_all_when_unspecified=True,
etat=None,
REQUEST=REQUEST,
)
groups = sco_groups.listgroups(context, groups_infos.group_ids)
gr_title_filename = sco_groups.listgroups_filename(groups)
2021-02-03 22:00:41 +01:00
# gr_title = sco_groups.listgroups_abbrev(groups)
2020-09-26 16:19:37 +02:00
if None in [g["group_name"] for g in groups]: # tous les etudiants
getallstudents = True
2021-02-03 22:00:41 +01:00
# gr_title = "tous"
2020-09-26 16:19:37 +02:00
gr_title_filename = "tous"
else:
getallstudents = False
etudids = sco_groups.do_evaluation_listeetuds_groups(
context, evaluation_id, groups, getallstudents=getallstudents, include_dems=True
)
# une liste de liste de chaines: lignes de la feuille de calcul
L = []
etuds = _get_sorted_etuds(context, E, etudids, formsemestre_id)
for e in etuds:
etudid = e["etudid"]
groups = sco_groups.get_etud_groups(context, etudid, sem)
grc = sco_groups.listgroups_abbrev(groups)
L.append(
[
"%s" % etudid,
scu.strupper(e["nom"]),
scu.strcapitalize(scu.strlower(e["prenom"])),
2020-09-26 16:19:37 +02:00
e["inscr"]["etat"],
grc,
e["val"],
e["explanation"],
]
)
filename = "notes_%s_%s.xls" % (evalname, gr_title_filename)
xls = sco_excel.Excel_feuille_saisie(E, sem["titreannee"], description, lines=L)
return sco_excel.sendExcelFile(REQUEST, xls, filename)
def has_existing_decision(context, M, E, etudid):
"""Verifie s'il y a une validation pour cet etudiant dans ce semestre ou UE
Si oui, return True
"""
formsemestre_id = M["formsemestre_id"]
nt = context._getNotesCache().get_NotesTable(
context, formsemestre_id
) # > get_etud_decision_sem, get_etud_decision_ues
if nt.get_etud_decision_sem(etudid):
return True
dec_ues = nt.get_etud_decision_ues(etudid)
if dec_ues:
mod = context.do_module_list({"module_id": M["module_id"]})[0]
ue_id = mod["ue_id"]
if ue_id in dec_ues:
return True # decision pour l'UE a laquelle appartient cette evaluation
return False # pas de decision de jury affectee par cette note
# -----------------------------
# Nouveau formulaire saisie notes (2016)
def saisie_notes(context, evaluation_id, group_ids=[], REQUEST=None):
2021-01-01 18:40:47 +01:00
"""Formulaire saisie notes d'une évaluation pour un groupe"""
2020-09-26 16:19:37 +02:00
authuser = REQUEST.AUTHENTICATED_USER
authusername = str(authuser)
evals = context.do_evaluation_list({"evaluation_id": evaluation_id})
if not evals:
raise ScoValueError("invalid evaluation_id")
E = evals[0]
M = sco_moduleimpl.do_moduleimpl_withmodule_list(
context, moduleimpl_id=E["moduleimpl_id"]
)[0]
2020-09-26 16:19:37 +02:00
formsemestre_id = M["formsemestre_id"]
# Check access
# (admin, respformation, and responsable_id)
if not can_edit_notes(context, authuser, E["moduleimpl_id"]):
2020-09-26 16:19:37 +02:00
return (
context.sco_header(REQUEST)
+ "<h2>Modification des notes impossible pour %s</h2>" % authusername
+ """<p>(vérifiez que le semestre n'est pas verrouillé et que vous
avez l'autorisation d'effectuer cette opération)</p>
<p><a href="moduleimpl_status?moduleimpl_id=%s">Continuer</a></p>
"""
% E["moduleimpl_id"]
+ context.sco_footer(REQUEST)
)
# Informations sur les groupes à afficher:
groups_infos = sco_groups_view.DisplayedGroupsInfos(
context,
group_ids=group_ids,
formsemestre_id=formsemestre_id,
select_all_when_unspecified=True,
etat=None,
REQUEST=REQUEST,
)
if E["description"]:
page_title = 'Saisie "%s"' % E["description"]
else:
page_title = "Saisie des notes"
# HTML page:
H = [
context.sco_header(
REQUEST,
page_title=page_title,
javascripts=sco_groups_view.JAVASCRIPTS + ["js/saisie_notes.js"],
cssstyles=sco_groups_view.CSSSTYLES,
init_qtip=True,
),
sco_evaluations.evaluation_describe(
context, evaluation_id=evaluation_id, REQUEST=REQUEST
),
'<div id="saisie_notes"><span class="eval_title">Saisie des notes</span>',
]
H.append("""<div id="group-tabs"><table><tr><td>""")
H.append(sco_groups_view.form_groups_choice(context, groups_infos))
H.append('</td><td style="padding-left: 35px;">')
H.append(
htmlutils.make_menu(
2020-09-26 16:19:37 +02:00
"Autres opérations",
[
{
"title": "Saisie par fichier tableur",
"id": "menu_saisie_tableur",
"url": "/saisie_notes_tableur?evaluation_id=%s&%s"
% (E["evaluation_id"], groups_infos.groups_query_args),
},
{
"title": "Voir toutes les notes du module",
"url": "/evaluation_listenotes?moduleimpl_id=%s"
% E["moduleimpl_id"],
},
{
"title": "Effacer toutes les notes de cette évaluation",
"url": "/evaluation_suppress_alln?evaluation_id=%s"
% (E["evaluation_id"],),
},
],
base_url=context.absolute_url(),
alone=True,
)
)
H.append("""</td></tr></table></div>""")
# Le formulaire de saisie des notes:
destination = "%s/Notes/moduleimpl_status?moduleimpl_id=%s" % (
context.ScoURL(),
E["moduleimpl_id"],
)
form = _form_saisie_notes(
context, E, M, groups_infos.group_ids, destination=destination, REQUEST=REQUEST
)
2020-09-26 16:19:37 +02:00
if form is None:
return REQUEST.RESPONSE.redirect(destination)
2020-09-26 16:19:37 +02:00
H.append(form)
#
H.append("</div>") # /saisie_notes
H.append(
"""<div class="sco_help">
<p>Les modifications sont enregistrées au fur et à mesure.</p>
<h4>Codes spéciaux:</h4>
<ul>
<li>ABS: absent (compte comme un zéro)</li>
<li>EXC: excusé (note neutralisée)</li>
<li>SUPR: pour supprimer une note existante</li>
<li>ATT: note en attente (permet de publier une évaluation avec des notes manquantes)</li>
</ul>
</div>"""
)
H.append(context.sco_footer(REQUEST))
return "\n".join(H)
def _get_sorted_etuds(context, E, etudids, formsemestre_id):
sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
NotesDB = context._notes_getall(E["evaluation_id"]) # Notes existantes
cnx = context.GetDBConnexion()
etuds = []
for etudid in etudids:
# infos identite etudiant
e = scolars.etudident_list(cnx, {"etudid": etudid})[0]
scolars.format_etud_ident(e)
etuds.append(e)
# infos inscription dans ce semestre
e["inscr"] = context.do_formsemestre_inscription_list(
{"etudid": etudid, "formsemestre_id": formsemestre_id}
)[0]
# Groupes auxquels appartient cet étudiant:
e["groups"] = sco_groups.get_etud_groups(context, etudid, sem)
# Information sur absence (tenant compte de la demi-journée)
jour_iso = DateDMYtoISO(E["jour"])
warn_abs_lst = []
if E["matin"]:
nbabs = context.Absences.CountAbs(etudid, jour_iso, jour_iso, matin=1)
nbabsjust = context.Absences.CountAbsJust(
etudid, jour_iso, jour_iso, matin=1
)
if nbabs:
if nbabsjust:
warn_abs_lst.append("absent justifié le matin !")
else:
warn_abs_lst.append("absent le matin !")
if E["apresmidi"]:
nbabs = context.Absences.CountAbs(etudid, jour_iso, jour_iso, matin=0)
nbabsjust = context.Absences.CountAbsJust(
etudid, jour_iso, jour_iso, matin=0
)
if nbabs:
if nbabsjust:
warn_abs_lst.append("absent justifié l'après-midi !")
else:
warn_abs_lst.append("absent l'après-midi !")
e["absinfo"] = '<span class="sn_abs">' + " ".join(warn_abs_lst) + "</span> "
# Note actuelle de l'étudiant:
if NotesDB.has_key(etudid):
e["val"] = _displayNote(NotesDB[etudid]["value"])
comment = NotesDB[etudid]["comment"]
if comment is None:
comment = ""
e["explanation"] = "%s (%s) %s" % (
NotesDB[etudid]["date"].strftime("%d/%m/%y %Hh%M"),
NotesDB[etudid]["uid"],
comment,
)
else:
e["val"] = ""
e["explanation"] = ""
# Démission ?
if e["inscr"]["etat"] == "D":
# if not e['val']:
e["val"] = "DEM"
e["explanation"] = "Démission"
etuds.sort(key=lambda x: (x["nom"], x["prenom"]))
return etuds
def _form_saisie_notes(context, E, M, group_ids, destination="", REQUEST=None):
2020-09-26 16:19:37 +02:00
"""Formulaire HTML saisie des notes dans l'évaluation E du moduleimpl M
pour les groupes indiqués.
On charge tous les étudiants, ne seront montrés que ceux
des groupes sélectionnés grace a un filtre en javascript.
"""
evaluation_id = E["evaluation_id"]
formsemestre_id = M["formsemestre_id"]
etudids = sco_groups.do_evaluation_listeetuds_groups(
context, evaluation_id, getallstudents=True, include_dems=True
)
if not etudids:
return '<div class="ue_warning"><span>Aucun étudiant sélectionné !</span></div>'
# Decisions de jury existantes ?
decisions_jury = {
etudid: has_existing_decision(context, M, E, etudid) for etudid in etudids
}
nb_decisions = sum(
decisions_jury.values()
) # Nb de decisions de jury (pour les inscrits à l'évaluation)
etuds = _get_sorted_etuds(context, E, etudids, formsemestre_id)
# Build form:
descr = [
("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}),
("formsemestre_id", {"default": formsemestre_id, "input_type": "hidden"}),
("group_ids", {"default": group_ids, "input_type": "hidden", "type": "list"}),
# ('note_method', { 'default' : note_method, 'input_type' : 'hidden'}),
("comment", {"size": 44, "title": "Commentaire", "return_focus_next": True}),
("changed", {"default": "0", "input_type": "hidden"}), # changed in JS
]
if M["module"]["module_type"] == scu.MODULE_STANDARD:
2020-09-26 16:19:37 +02:00
descr.append(
(
"s3",
{
"input_type": "text", # affiche le barème
"title": "Notes ",
"cssclass": "formnote_bareme",
"readonly": True,
"default": "&nbsp;/ %g" % E["note_max"],
},
)
)
elif M["module"]["module_type"] == scu.MODULE_MALUS:
2020-09-26 16:19:37 +02:00
descr.append(
(
"s3",
{
"input_type": "text", # affiche le barème
"title": "",
"cssclass": "formnote_bareme",
"readonly": True,
"default": "Points de malus (soustraits à la moyenne de l'UE, entre -20 et 20)",
},
)
)
else:
raise ValueError("invalid module type (%s)" % M["module"]["module_type"]) # bug
initvalues = {}
for e in etuds:
etudid = e["etudid"]
disabled = e["val"] == "DEM"
etud_classes = []
if disabled:
classdem = " etud_dem"
etud_classes.append("etud_dem")
disabled_attr = 'disabled="%d"' % disabled
else:
classdem = ""
disabled_attr = ""
# attribue a chaque element une classe css par groupe:
for group_info in e["groups"]:
etud_classes.append(group_info["group_id"])
label = '<span class="%s">' % classdem + e["nomprenom"] + "</span>"
# Historique des saisies de notes:
if not disabled:
explanation = (
'<span id="hist_%s">' % etudid
+ get_note_history_menu(context, evaluation_id, etudid)
+ "</span>"
)
else:
explanation = ""
explanation = e["absinfo"] + explanation
# Lien modif decision de jury:
explanation += '<span id="jurylink_%s" class="jurylink"></span>' % etudid
# Valeur actuelle du champ:
initvalues["note_" + etudid] = e["val"]
label_link = '<a class="etudinfo" id="%s">%s</a>' % (etudid, label)
# Element de formulaire:
descr.append(
(
"note_" + etudid,
{
"size": 5,
"title": label_link,
"explanation": explanation,
"return_focus_next": True,
"attributes": [
'class="note%s"' % classdem,
disabled_attr,
"data-last-saved-value=%s" % e["val"],
"data-orig-value=%s" % e["val"],
"data-etudid=%s" % etudid,
],
"template": """<tr%(item_dom_attr)s class="etud_elem """
+ " ".join(etud_classes)
+ """"><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s</td></tr>""",
},
)
)
#
H = []
if nb_decisions > 0:
H.append(
"""<div class="saisie_warn">
<ul class="tf-msg">
<li class="tf-msg">Attention: il y a déjà des <b>décisions de jury</b> enregistrées pour %d étudiants. Après changement des notes, vérifiez la situation !</li>
</ul>
</div>"""
% nb_decisions
)
# H.append('''<div id="sco_msg" class="head_message"></div>''')
tf = TF(
destination,
REQUEST.form,
descr,
initvalues=initvalues,
submitlabel="Terminer",
formid="formnotes",
)
H.append(tf.getform()) # check and init
if tf.canceled():
return None
elif (not tf.submitted()) or not tf.result:
# ajout formularie saisie notes manquantes
H.append(
"""
<div>
<form action="do_evaluation_set_missing" method="get">
Mettre <em>toutes</em> les notes manquantes à <input type="text" size="5" name="value"/>
<input type="submit" value="OK"/>
<input type="hidden" name="evaluation_id" value="%s"/>
<em>affecte tous les groupes. ABS indique "absent" (zéro), EXC "excusé" (neutralisées), ATT "attente"</em>
</form>
</div>
"""
% evaluation_id
)
# affiche formulaire
return "\n".join(H)
else:
# form submission
# rien à faire
return None
def save_note(
context, etudid=None, evaluation_id=None, value=None, comment="", REQUEST=None
):
2021-01-01 18:40:47 +01:00
"""Enregistre une note (ajax)"""
2020-09-26 16:19:37 +02:00
authuser = REQUEST.AUTHENTICATED_USER
log(
"save_note: evaluation_id=%s etudid=%s uid=%s value=%s"
% (evaluation_id, etudid, authuser, value)
)
E = context.do_evaluation_list({"evaluation_id": evaluation_id})[0]
M = sco_moduleimpl.do_moduleimpl_list(context, moduleimpl_id=E["moduleimpl_id"])[0]
2020-09-26 16:19:37 +02:00
Mod = context.do_module_list(args={"module_id": M["module_id"]})[0]
Mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % M
result = {"nbchanged": 0} # JSON
# Check access: admin, respformation, or responsable_id
if not can_edit_notes(context, authuser, E["moduleimpl_id"]):
2020-09-26 16:19:37 +02:00
result["status"] = "unauthorized"
else:
2021-02-03 22:00:41 +01:00
L, _, _, _, _ = _check_notes([(etudid, value)], E, Mod)
2020-09-26 16:19:37 +02:00
if L:
2021-02-03 22:00:41 +01:00
nbchanged, _, existing_decisions = _notes_add(
2020-09-26 16:19:37 +02:00
context, authuser, evaluation_id, L, comment=comment, do_it=True
)
sco_news.add(
context,
REQUEST,
typ=NEWS_NOTE,
object=M["moduleimpl_id"],
text='Chargement notes dans <a href="%(url)s">%(titre)s</a>' % Mod,
url=Mod["url"],
max_frequency=30 * 60, # 30 minutes
)
result["nbchanged"] = nbchanged
result["existing_decisions"] = existing_decisions
if nbchanged > 0:
result["history_menu"] = get_note_history_menu(
context, evaluation_id, etudid
)
else:
result["history_menu"] = "" # no update needed
result["status"] = "ok"
return scu.sendJSON(REQUEST, result)
2020-09-26 16:19:37 +02:00
def get_note_history_menu(context, evaluation_id, etudid):
"""Menu HTML historique de la note"""
history = sco_undo_notes.get_note_history(context, evaluation_id, etudid)
if not history:
return ""
H = []
if len(history) > 1:
H.append(
'<select data-etudid="%s" class="note_history" onchange="change_history(this);">'
% etudid
)
envir = "select"
item = "option"
else:
# pas de menu
H.append('<span class="history">')
envir = "span"
item = "span"
first = True
for i in history:
jt = i["date"].strftime("le %d/%m/%Y à %H:%M") + " (%s)" % i["user_name"]
dispnote = _displayNote(i["value"])
if first:
nv = "" # ne repete pas la valeur de la note courante
else:
# ancienne valeur
nv = '<span class="histvalue">: %s</span>' % dispnote
first = False
if i["comment"]:
comment = ' <span class="histcomment">%s</span>' % i["comment"]
else:
comment = ""
H.append(
'<%s data-note="%s">%s %s%s</%s>' % (item, dispnote, jt, nv, comment, item)
)
H.append("</%s>" % envir)
return "\n".join(H)