# -*- 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@viennet.net
#
##############################################################################
"""Saisie des notes
Formulaire revu en juillet 2016
"""
import time
import datetime
import psycopg2
import sco_utils as scu
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
import sco_core
import sco_formsemestre
import sco_moduleimpl
import sco_groups
import sco_groups_view
import sco_evaluations
import sco_parcours_dut
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(Permission.ScoEditAllNotes, context)
or uid in sem["responsables"]
)
else:
if (
(not authuser.has_permission(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
def convert_note_from_string(
note,
note_max,
note_min=scu.NOTES_MIN,
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
elif note[:3] == "ATT":
note_value = scu.NOTES_ATTENTE
elif note[:3] == "SUP":
note_value = scu.NOTES_SUPPRESS
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:
val = "EXC" # excuse, note neutralise
elif val == scu.NOTES_ATTENTE:
val = "ATT" # attente, note neutralise
elif val == scu.NOTES_SUPPRESS:
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:
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]
# Check access
# (admin, respformation, and responsable_id)
if not can_edit_notes(context, authuser, E["moduleimpl_id"]):
# 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)
"%s"'
% (ni, str(lines[ni]))
)
raise InvalidNoteValue()
# -- check values
L, invalids, withoutnotes, absents, _ = _check_notes(notes, E, M["module"])
if len(invalids):
diag.append(
"Erreur: la feuille contient %d notes invalides
%d notes changées (%d sans notes, %d absents, %d note supprimées)
" % (nb_changed, len(withoutnotes), len(absents), nb_suppress) ) if existing_decisions: msg += """Important: il y avait déjà des décisions de jury enregistrées, qui sont potentiellement à revoir suite à cette modification !
""" # msg += '' + str(notes) # debug return 1, msg except InvalidNoteValue: if diag: msg = ( '
(pas de notes modifiées)
" def do_evaluation_set_missing( context, evaluation_id, value, REQUEST=None, dialog_confirmed=False ): """Initialisation des notes manquantes""" 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] # Check access # (admin, respformation, and responsable_id) if not can_edit_notes(context, authuser, E["moduleimpl_id"]): # 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 L, invalids, _, _, _ = _check_notes(notes, E, M["module"]) diag = "" if len(invalids): diag = "Valeur %s invalide" % value if diag: return ( html_sco_header.sco_header(context, REQUEST) + 'Recommencer' % (diag, evaluation_id) + html_sco_header.sco_footer(context, REQUEST) ) # Confirm action if not dialog_confirmed: return scu.confirm_dialog( context, """
Seuls les étudiants pour lesquels aucune note (ni valeur, ni ABS, ni EXC) n'a été rentrée seront affectés.
%d étudiants concernés par ce changement de note.
Attention, les étudiants sans notes de tous les groupes de ce semestre seront affectés.
""" % (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" nb_changed, _, _ = _notes_add(context, authuser, evaluation_id, L, comment) # news M = sco_moduleimpl.do_moduleimpl_list(context, moduleimpl_id=E["moduleimpl_id"])[0] 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 %(titre)s' % mod, url=mod["url"], ) return ( html_sco_header.sco_header(context, REQUEST) + """Confirmer la suppression des %d notes ?
" % nb_suppress if existing_decisions: msg += """Important: il y a déjà des décisions de jury enregistrées, qui seront potentiellement à revoir suite à cette modification !
""" return scu.confirm_dialog( context, 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 = ["%s notes supprimées
" % nb_suppress] if existing_decisions: H.append( """Important: il y avait déjà des décisions de jury enregistrées, qui sont potentiellement à revoir suite à cette modification !
""" ) H += [ 'continuer' % E["moduleimpl_id"] ] # news M = sco_moduleimpl.do_moduleimpl_list(context, moduleimpl_id=E["moduleimpl_id"])[0] 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 %(titre)s' % mod, url=mod["url"], ) return ( html_sco_header.sco_header(context, REQUEST) + "\n".join(H) + html_sco_header.sco_footer(context, 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 _ = {}.fromkeys( 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] 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: 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 ): 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: 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 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 sco_core.inval_cache( context, formsemestre_id=M["formsemestre_id"] ) # > modif notes (exception) cnx.rollback() # abort raise # re-raise exception if do_it: cnx.commit() sco_core.inval_cache( context, formsemestre_id=M["formsemestre_id"] ) # > modif notes sco_core.get_evaluations_cache( context, ).inval_cache(key=evaluation_id) return nb_changed, nb_suppress, existing_decisions def saisie_notes_tableur(context, evaluation_id, group_ids=[], REQUEST=None): """Saisie des notes via un fichier Excel""" 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_list(context, moduleimpl_id=E["moduleimpl_id"])[0] formsemestre_id = M["formsemestre_id"] if not can_edit_notes(context, authuser, E["moduleimpl_id"]): return ( html_sco_header.sco_header(context, REQUEST) + "
(vérifiez que le semestre n'est pas verrouillé et que vous avez l'autorisation d'effectuer cette opération)
""" % E["moduleimpl_id"] + html_sco_header.sco_footer(context, 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 = [ html_sco_header.sco_header( context, 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 ), """Saisie des notes par fichier""", ] # Menu choix groupe: H.append("""""") H.append(sco_groups_view.form_groups_choice(context, groups_infos)) H.append(" |
Le fichier doit être un fichier tableur obtenu via l'étape 1 ci-dessus, puis complété et enregistré au format Excel.
""" ) H.append(nf[1]) elif nf[0] == -1: H.append("Annulation
") elif nf[0] == 1: updiag = do_evaluation_upload_xls(context, REQUEST) if updiag[0]: H.append(updiag[1]) H.append( """Notes chargées. Revenir au tableau de bord du module Charger d'autres notes dans cette évaluation
""" % E ) else: H.append("""Notes non chargées !
""" + updiag[1]) H.append( """ """ % E ) # H.append("""(vérifiez que le semestre n'est pas verrouillé et que vous avez l'autorisation d'effectuer cette opération)
""" % E["moduleimpl_id"] + html_sco_header.sco_footer(context, 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 = [ html_sco_header.sco_header( context, 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 ), '""") H.append(sco_groups_view.form_groups_choice(context, groups_infos)) H.append(' | ') H.append( htmlutils.make_menu( "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(""" |
Les modifications sont enregistrées au fur et à mesure.