# -*- 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 datetime
from notesdb import *
from sco_utils import *
from notes_log import log
from TrivialFormulator import TrivialFormulator, TF
from notes_table import *
import sco_formsemestre
import sco_groups
import sco_groups_view
from sco_formsemestre_status import makeMenu
import sco_evaluations
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 convert_note_from_string(
note,
note_max,
note_min=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 = NOTES_NEUTRALISE
elif note[:3] == "ATT":
note_value = NOTES_ATTENTE
elif note[:3] == "SUP":
note_value = 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 == NOTES_NEUTRALISE:
val = "EXC" # excuse, note neutralise
elif val == NOTES_ATTENTE:
val = "ATT" # attente, note neutralise
elif val == 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"] == MODULE_STANDARD:
note_min = NOTES_MIN
elif mod["module_type"] == 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 = context.do_moduleimpl_withmodule_list(moduleimpl_id=E["moduleimpl_id"])[0]
# Check access
# (admin, respformation, and responsable_id)
if not context.can_edit_notes(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
ok = True
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, tosuppress = _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 = context.do_moduleimpl_withmodule_list(moduleimpl_id=E["moduleimpl_id"])[0] # Check access # (admin, respformation, and responsable_id) if not context.can_edit_notes(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, withoutnotes, absents, tosuppress = _check_notes(notes, E, M["module"]) diag = "" if len(invalids): diag = "Valeur %s invalide" % value if diag: return ( context.sco_header(REQUEST) + 'Recommencer' % (diag, evaluation_id) + context.sco_footer(REQUEST) ) # Confirm action if not dialog_confirmed: return context.confirmDialog( """
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, nb_suppress, existing_decisions = _notes_add( context, authuser, evaluation_id, L, comment ) # news cnx = context.GetDBConnexion() M = context.do_moduleimpl_list(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 ( context.sco_header(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 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 = ["%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 = context.do_moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] mod = context.do_module_list(args={"module_id": M["module_id"]})[0] mod["moduleimpl_id"] = M["moduleimpl_id"] cnx = context.GetDBConnexion() 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 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 inscrits = {}.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 = context.do_moduleimpl_list(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 != 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) > 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 != 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"] = 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 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): """Saisie des notes via un fichier Excel """ authuser = REQUEST.AUTHENTICATED_USER evals = context.do_evaluation_list({"evaluation_id": evaluation_id}) if not evals: raise ScoValueError("invalid evaluation_id") E = evals[0] M = context.do_moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] formsemestre_id = M["formsemestre_id"] if not context.can_edit_notes(authuser, E["moduleimpl_id"]): return ( context.sco_header(REQUEST) + "
(vérifiez que le semestre n'est pas verrouillé et que vous avez l'autorisation d'effectuer cette opération)
""" % 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 ), """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"] + 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 ), '""") H.append(sco_groups_view.form_groups_choice(context, groups_infos)) H.append(' | ') H.append( makeMenu( "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.