# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2020 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

" % 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 cnx = context.GetDBConnexion() E = context.do_evaluation_list({"evaluation_id": evaluation_id})[0] 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='Chargement notes dans %(titre)s' % mod, url=mod["url"], ) msg = ( "

%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 = ( '

" ) else: msg = '' return 0, 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) + '

%s

Recommencer' % (diag, evaluation_id) + context.sco_footer(REQUEST) ) # Confirm action if not dialog_confirmed: return context.confirmDialog( """

Mettre toutes les notes manquantes de l'évaluation à la valeur %s ?

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) + """

%d notes changées

""" % (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 context.can_edit_notes(authuser, E["moduleimpl_id"], allow_ens=False): # On a le droit de modifier toutes les notes # recupere les etuds ayant une note NotesDB = context._notes_getall(evaluation_id) elif context.can_edit_notes(authuser, E["moduleimpl_id"], allow_ens=True): # 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, NOTES_SUPPRESS) for etudid in NotesDB.keys()] if not dialog_confirmed: nb_changed, nb_suppress, existing_decisions = _notes_add( context, authuser, evaluation_id, notes, do_it=False ) msg = "

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) + "

Modification des notes impossible pour %s

" % authusername + """

(vérifiez que le semestre n'est pas verrouillé et que vous avez l'autorisation d'effectuer cette opération)

Continuer

""" % 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("
") H.append( """
Etape 1 :
""" % (evaluation_id, groups_infos.groups_query_args, evaluation_id, evaluation_id) ) H.append( """
Etape 2 : chargement d'un fichier de notes""" #' ) 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( """

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( """

Reprendre

""" % E ) # H.append("""

Autres opérations

""" % E ) H.append( """

Explications

  1. Etape 1:
    1. choisir le ou les groupes d'étudiants;
    2. télécharger le fichier Excel à remplir.
  2. Etape 2 (cadre vert): Indiquer le fichier Excel téléchargé à l'étape 1 et dans lequel on a saisi des notes. Remarques:
""" ) H.append(context.sco_footer(REQUEST)) return "\n".join(H) def feuille_saisie_notes(context, evaluation_id, group_ids=[], REQUEST=None): """Document Excel pour saisie notes dans l'évaluation et les groupes indiqués """ 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"] 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 = sanitize_filename(E["description"])[:12] 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"], strcapitalize(M["responsable_id"]), ) 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) gr_title = sco_groups.listgroups_abbrev(groups) if None in [g["group_name"] for g in groups]: # tous les etudiants getallstudents = True gr_title = "tous" gr_title_filename = "tous" else: getallstudents = False etudids = sco_groups.do_evaluation_listeetuds_groups( context, evaluation_id, groups, getallstudents=getallstudents, include_dems=True ) # Notes existantes NotesDB = context._notes_getall(evaluation_id) # 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, strupper(e["nom"]), strcapitalize(strlower(e["prenom"])), 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): """Formulaire saisie notes d'une évaluation pour un groupe """ 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 = context.do_moduleimpl_withmodule_list(moduleimpl_id=E["moduleimpl_id"])[0] formsemestre_id = M["formsemestre_id"] # Check access # (admin, respformation, and responsable_id) if not context.can_edit_notes(authuser, E["moduleimpl_id"]): return ( context.sco_header(REQUEST) + "

Modification des notes impossible pour %s

" % authusername + """

(vérifiez que le semestre n'est pas verrouillé et que vous avez l'autorisation d'effectuer cette opération)

Continuer

""" % 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 ), '
Saisie des notes', ] H.append("""
""") 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("""
""") # Le formulaire de saisie des notes: form = _form_saisie_notes(context, E, M, groups_infos.group_ids, REQUEST=REQUEST) if form is None: return "" # redirect H.append(form) # H.append("
") # /saisie_notes H.append( """

Les modifications sont enregistrées au fur et à mesure.

Codes spéciaux:

""" ) 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"] = '' + " ".join(warn_abs_lst) + " " # 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, REQUEST=None): """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"] groups = sco_groups.listgroups(context, group_ids) etudids = sco_groups.do_evaluation_listeetuds_groups( context, evaluation_id, getallstudents=True, include_dems=True ) if not etudids: return '
Aucun étudiant sélectionné !
' # 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"] == MODULE_STANDARD: descr.append( ( "s3", { "input_type": "text", # affiche le barème "title": "Notes ", "cssclass": "formnote_bareme", "readonly": True, "default": " / %g" % E["note_max"], }, ) ) elif M["module"]["module_type"] == MODULE_MALUS: 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 = '' % classdem + e["nomprenom"] + "" # Historique des saisies de notes: if not disabled: explanation = ( '' % etudid + get_note_history_menu(context, evaluation_id, etudid) + "" ) else: explanation = "" explanation = e["absinfo"] + explanation # Lien modif decision de jury: explanation += '' % etudid # Valeur actuelle du champ: initvalues["note_" + etudid] = e["val"] label_link = '%s' % (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": """%(label)s%(elem)s""", }, ) ) # H = [] if nb_decisions > 0: H.append( """
""" % nb_decisions ) # H.append('''
''') destination = "%s/Notes/moduleimpl_status?moduleimpl_id=%s" % ( context.ScoURL(), M["moduleimpl_id"], ) tf = TF( destination, REQUEST.form, descr, initvalues=initvalues, submitlabel="Terminer", formid="formnotes", ) H.append(tf.getform()) # check and init if tf.canceled(): REQUEST.RESPONSE.redirect(destination) return None elif (not tf.submitted()) or not tf.result: # ajout formularie saisie notes manquantes H.append( """
Mettre toutes les notes manquantes à affecte tous les groupes. ABS indique "absent" (zéro), EXC "excusé" (neutralisées), ATT "attente"
""" % evaluation_id ) # affiche formulaire return "\n".join(H) else: # form submission # rien à faire REQUEST.RESPONSE.redirect(destination) return None def save_note( context, etudid=None, evaluation_id=None, value=None, comment="", REQUEST=None ): """Enregistre une note (ajax) """ 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 = context.do_moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] 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 context.can_edit_notes(authuser, E["moduleimpl_id"]): result["status"] = "unauthorized" else: L, invalids, withoutnotes, absents, tosuppress = _check_notes( [(etudid, value)], E, Mod ) if L: nbchanged, nbsuppress, existing_decisions = _notes_add( 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 %(titre)s' % 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" # time.sleep(5) return sendJSON(REQUEST, result) 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( '