############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2024 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 html import time import flask from flask import g, url_for from flask_login import current_user from flask_sqlalchemy.query import Query import psycopg2 from app import db, log from app.auth.models import User from app.comp import res_sem from app.comp.res_compat import NotesTableCompat from app.models import ( Evaluation, FormSemestre, Module, ModuleImpl, ScolarNews, Assiduite, ) from app.models.etudiants import Identite from app.scodoc.sco_exceptions import ( AccessDenied, NoteProcessError, ScoException, ScoInvalidParamError, ScoValueError, ) from app.scodoc import html_sco_header from app.scodoc import htmlutils from app.scodoc import sco_cache from app.scodoc import sco_etud from app.scodoc import sco_evaluation_db from app.scodoc import sco_evaluations from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_groups from app.scodoc import sco_groups_view from app.scodoc import sco_undo_notes import app.scodoc.notesdb as ndb from app.scodoc.TrivialFormulator import TF import app.scodoc.sco_utils as scu from app.scodoc.sco_utils import json_error from app.scodoc.sco_utils import ModuleType def convert_note_from_string( note: str, note_max: float, note_min: float = scu.NOTES_MIN, etudid: int = None, absents: list[int] = None, invalids: list[int] = None, ) -> tuple[float, bool]: """converti une valeur (chaine saisie) vers une note numérique (float) Les listes absents et invalids sont modifiées. Return: note_value: float (valeur de la note ou code EXC, ATT, ...) invalid: True si note invalide (eg hors barème) """ 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 else: try: note_value = float(note) if (note_value < note_min) or (note_value > note_max): raise ValueError except ValueError: invalids.append(etudid) invalid = True return note_value, invalid def check_notes( notes: list[(int, float | str)], evaluation: Evaluation ) -> tuple[list[tuple[int, float]], list[int], list[int], list[int], list[int]]: """Vérifie et converti les valeurs des notes pour une évaluation. notes: list of tuples (etudid, value) evaluation: target Returns valid_notes: list of valid notes (etudid, float value) and 4 lists of etudid: etudids_invalids : etudid avec notes invalides etudids_without_notes: etudid sans notes (champs vides) etudids_absents : etudid avec note ABS etudids_non_inscrits : etudid non inscrits à ce module (ne considère pas l'inscr. au semestre) """ note_max = evaluation.note_max or 0.0 module: Module = evaluation.moduleimpl.module if module.module_type in ( scu.ModuleType.STANDARD, scu.ModuleType.RESSOURCE, scu.ModuleType.SAE, ): if evaluation.evaluation_type == Evaluation.EVALUATION_BONUS: note_min, note_max = -20, 20 else: note_min = scu.NOTES_MIN elif module.module_type == ModuleType.MALUS: note_min = -20.0 else: raise ValueError("Invalid module type") # bug # Vérifie inscription au module (même DEM/DEF) etudids_inscrits_mod = { i.etudid for i in evaluation.moduleimpl.query_inscriptions() } valid_notes = [] etudids_invalids = [] etudids_without_notes = [] etudids_absents = [] etudids_non_inscrits = [] for etudid, note in notes: if etudid not in etudids_inscrits_mod: etudids_non_inscrits.append(etudid) continue try: etudid = int(etudid) # except ValueError as exc: raise ScoValueError(f"Code étudiant ({etudid}) invalide") from exc 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=etudids_absents, invalids=etudids_invalids, ) if not invalid: valid_notes.append((etudid, value)) else: etudids_without_notes.append(etudid) return ( valid_notes, etudids_invalids, etudids_without_notes, etudids_absents, etudids_non_inscrits, ) def do_evaluation_set_etud_note(evaluation: Evaluation, etud: Identite, value) -> bool: """Enregistre la note d'un seul étudiant value: valeur externe (float ou str) """ if not evaluation.moduleimpl.can_edit_notes(current_user): raise AccessDenied(f"Modification des notes impossible pour {current_user}") # Convert and check value notes, invalids, _, _, _ = check_notes([(etud.id, value)], evaluation) if len(invalids) == 0: etudids_changed, _, _, _ = notes_add( current_user, evaluation.id, notes, "Initialisation notes" ) if len(etudids_changed) == 1: return True return False # error def do_evaluation_set_missing( evaluation_id, value, dialog_confirmed=False, group_ids_str: str = "" ): """Initialisation des notes manquantes""" evaluation = Evaluation.query.get_or_404(evaluation_id) modimpl = evaluation.moduleimpl # Check access # (admin, respformation, and responsable_id) if not modimpl.can_edit_notes(current_user): raise AccessDenied(f"Modification des notes impossible pour {current_user}") # notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id) if not group_ids_str: groups = None else: group_ids = [int(x) for x in str(group_ids_str).split(",")] groups = sco_groups.listgroups(group_ids) etudid_etats = sco_groups.do_evaluation_listeetuds_groups( evaluation_id, getallstudents=groups is None, groups=groups, include_demdef=False, ) notes = [] for etudid, _ in etudid_etats: # pour tous les inscrits if etudid not in notes_db: # pas de note notes.append((etudid, value)) # Convert and check values valid_notes, invalids, _, _, _ = check_notes(notes, evaluation) dest_url = url_for( "notes.saisie_notes", scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id ) diag = "" if len(invalids) > 0: diag = f"Valeur {value} invalide ou hors barème" if diag: return f""" {html_sco_header.sco_header()}

{diag}

Recommencer

{html_sco_header.sco_footer()} """ # Confirm action if not dialog_confirmed: plural = len(valid_notes) > 1 return scu.confirm_dialog( f"""

Mettre toutes les notes manquantes de l'évaluation à la valeur {value} ?

Seuls les étudiants pour lesquels aucune note (ni valeur, ni ABS, ni EXC) n'a été rentrée seront affectés.

{len(valid_notes)} étudiant{"s" if plural else ""} concerné{"s" if plural else ""} par ce changement de note.

""", dest_url="", cancel_url=dest_url, parameters={ "evaluation_id": evaluation_id, "value": value, "group_ids_str": group_ids_str, }, ) # ok comment = "Initialisation notes manquantes" etudids_changed, _, _, _ = notes_add( current_user, evaluation_id, valid_notes, comment ) # news url = url_for( "notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=evaluation.moduleimpl_id, ) ScolarNews.add( typ=ScolarNews.NEWS_NOTE, obj=evaluation.moduleimpl_id, text=f"""Initialisation notes dans {modimpl.module.titre or ""}""", url=url, max_frequency=30 * 60, ) return f""" { html_sco_header.sco_header() }

{len(etudids_changed)} notes changées

{ html_sco_header.sco_footer() } """ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False): "suppress all notes in this eval" evaluation = Evaluation.query.get_or_404(evaluation_id) if evaluation.moduleimpl.can_edit_notes(current_user, allow_ens=False): # On a le droit de modifier toutes les notes # recupere les etuds ayant une note notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id) elif evaluation.moduleimpl.can_edit_notes(current_user, allow_ens=True): # Enseignant associé au module: ne peut supprimer que les notes qu'il a saisi notes_db = sco_evaluation_db.do_evaluation_get_all_notes( evaluation_id, by_uid=current_user.id ) else: raise AccessDenied(f"Modification des notes impossible pour {current_user}") notes = [(etudid, scu.NOTES_SUPPRESS) for etudid in notes_db.keys()] status_url = url_for( "notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=evaluation.moduleimpl_id, ) if not dialog_confirmed: etudids_changed, nb_suppress, existing_decisions, _ = notes_add( current_user, evaluation_id, notes, do_it=False, check_inscription=False ) msg = f"""

Confirmer la suppression des {nb_suppress} notes ? (peut affecter plusieurs groupes)

""" 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( msg, dest_url="", OK="Supprimer les notes", cancel_url=status_url, parameters={"evaluation_id": evaluation_id}, ) # modif etudids_changed, nb_suppress, existing_decisions, _ = notes_add( current_user, evaluation_id, notes, comment="effacer tout", check_inscription=False, ) assert len(etudids_changed) == nb_suppress H = [f"""

{nb_suppress} notes supprimées

"""] 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 += [ f"""

continuer """ ] # news if nb_suppress: ScolarNews.add( typ=ScolarNews.NEWS_NOTE, obj=evaluation.moduleimpl.id, text=f"""Suppression des notes d'une évaluation dans {evaluation.moduleimpl.module.titre or 'module sans titre'} """, url=status_url, ) return html_sco_header.sco_header() + "\n".join(H) + html_sco_header.sco_footer() def notes_add( user: User, evaluation_id: int, notes: list, comment=None, do_it=True, check_inscription=True, ) -> tuple[list[int], int, list[int], list[str]]: """ 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) Raise NoteProcessError si note invalide ou étudiant non inscrit. Return: tuple (etudids_changed, nb_suppress, etudids_with_decision, messages) messages = list de messages d'avertissement/information pour l'utilisateur """ evaluation = Evaluation.get_evaluation(evaluation_id) now = psycopg2.Timestamp(*time.localtime()[:6]) messages = [] # Vérifie inscription au module (même DEM/DEF) etudids_inscrits_mod = { i.etudid for i in evaluation.moduleimpl.query_inscriptions() } # Les étudiants inscrits au semestre et ceux "actifs" (ni DEM ni DEF) etudids_inscrits_sem, etudids_actifs = ( evaluation.moduleimpl.formsemestre.etudids_actifs() ) for etudid, value in notes: if check_inscription: msg_err, msg_warn = "", "" if etudid not in etudids_inscrits_sem: msg_err = "non inscrit au semestre" elif etudid not in etudids_inscrits_mod: msg_err = "non inscrit au module" elif etudid not in etudids_actifs: # DEM ou DEF msg_warn = "démissionnaire ou défaillant (note enregistrée)" if msg_err or msg_warn: etud = Identite.query.get(etudid) if isinstance(etudid, int) else None msg = f"étudiant {etud.nomprenom if etud else etudid} {msg_err or msg_warn}" if msg_err: log(f"notes_add: {etudid} non inscrit ou DEM/DEF: aborting") raise NoteProcessError(msg) if msg_warn: messages.append(msg) if (value is not None) and not isinstance(value, float): log(f"notes_add: {etudid} valeur de note invalide ({value}): aborting") etud = Identite.query.get(etudid) if isinstance(etudid, int) else None raise NoteProcessError( f"etudiant {etud.nomprenom if etud else etudid}: valeur de note invalide ({value})" ) # Recherche notes existantes notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id) # Met a jour la base cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) etudids_changed = [] nb_suppress = 0 formsemestre: FormSemestre = evaluation.moduleimpl.formsemestre res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) # etudids pour lesquels il y a une decision de jury et que la note change: etudids_with_decision = [] try: for etudid, value in notes: changed, suppressed = _record_note( cursor, notes_db, etudid, evaluation_id, value, comment=comment, user=user, date=now, do_it=do_it, ) if suppressed: nb_suppress += 1 if changed: etudids_changed.append(etudid) if res.etud_has_decision(etudid, include_rcues=False): etudids_with_decision.append(etudid) except NotImplementedError as exc: # XXX log("*** exception in notes_add") if do_it: cnx.rollback() # abort # inval cache sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id) sco_cache.EvaluationCache.delete(evaluation_id) raise ScoException from exc if do_it: cnx.commit() sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id) sco_cache.EvaluationCache.delete(evaluation_id) return etudids_changed, nb_suppress, etudids_with_decision, messages def _record_note( cursor, notes_db, etudid: int, evaluation_id: int, value: float, comment: str = "", user: User = None, date=None, do_it=False, ): "Enregistrement de la note en base" changed = False suppressed = False args = { "etudid": etudid, "evaluation_id": evaluation_id, "value": value, # convention scodoc7 quote comment: "comment": (html.escape(comment) if isinstance(comment, str) else comment), "uid": user.id, "date": date, } if etudid not in notes_db: # nouvelle note if value != scu.NOTES_SUPPRESS: if do_it: # Note: le conflit ci-dessous peut arriver si un autre thread # a modifié la base après qu'on ait lu notes_db 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) ON CONFLICT ON CONSTRAINT notes_notes_etudid_evaluation_id_key DO UPDATE SET etudid=%(etudid)s, evaluation_id=%(evaluation_id)s, value=%(value)s, comment=%(comment)s, date=%(date)s, uid=%(uid)s """, args, ) changed = True else: # il y a deja une note oldval = notes_db[etudid]["value"] changed = ( (not isinstance(value, type(oldval))) or ( isinstance(value, float) and (abs(value - oldval) > scu.NOTES_PRECISION) ) or value != oldval ) 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 """, args, ) 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 """, args, ) else: # suppression ancienne note if do_it: log( f"""notes_add, suppress, evaluation_id={evaluation_id}, etudid={ etudid}, oldval={oldval}""" ) cursor.execute( """DELETE FROM notes_notes WHERE etudid = %(etudid)s AND evaluation_id = %(evaluation_id)s """, args, ) # garde trace de la suppression dans l'historique: args["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) """, args, ) suppressed = True return changed, suppressed # Nouveau formulaire saisie notes (2016) def saisie_notes(evaluation_id: int, group_ids: list = None): """Formulaire saisie notes d'une évaluation pour un groupe""" if not isinstance(evaluation_id, int): raise ScoInvalidParamError() group_ids = [int(group_id) for group_id in (group_ids or [])] evaluation: Evaluation = db.session.get(Evaluation, evaluation_id) if evaluation is None: raise ScoValueError("évaluation inexistante") modimpl = evaluation.moduleimpl moduleimpl_status_url = url_for( "notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=evaluation.moduleimpl_id, ) # Check access # (admin, respformation, and responsable_id) if not evaluation.moduleimpl.can_edit_notes(current_user): return f""" {html_sco_header.sco_header()}

Modification des notes impossible pour {current_user.user_name}

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

Continuer

{html_sco_header.sco_footer()} """ # Informations sur les groupes à afficher: groups_infos = sco_groups_view.DisplayedGroupsInfos( group_ids=group_ids, formsemestre_id=modimpl.formsemestre_id, select_all_when_unspecified=True, etat=None, ) page_title = ( f'Saisie "{evaluation.description}"' if evaluation.description else "Saisie des notes" ) # HTML page: H = [ html_sco_header.sco_header( 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( evaluation_id=evaluation_id, link_saisie=False ), '
Saisie des notes', ] H.append("""
""") H.append(sco_groups_view.form_groups_choice(groups_infos)) H.append('') H.append( htmlutils.make_menu( "Autres opérations", [ { "title": "Saisir par fichier tableur", "id": "menu_saisie_tableur", "endpoint": "notes.saisie_notes_tableur", "args": { "evaluation_id": evaluation.id, "group_ids": groups_infos.group_ids, }, }, { "title": "Voir toutes les notes du module", "endpoint": "notes.evaluation_listenotes", "args": {"moduleimpl_id": evaluation.moduleimpl_id}, }, { "title": "Effacer toutes les notes de cette évaluation", "endpoint": "notes.evaluation_suppress_alln", "args": {"evaluation_id": evaluation.id}, }, ], alone=True, ) ) H.append( """
""" ) # Le formulaire de saisie des notes: form = _form_saisie_notes( evaluation, modimpl, groups_infos, destination=moduleimpl_status_url ) if form is None: return flask.redirect(moduleimpl_status_url) H.append(form) # H.append("
") # /saisie_notes H.append( """

Les modifications sont enregistrées au fur et à mesure. Vous pouvez aussi copier/coller depuis un tableur ou autre logiciel.

Codes spéciaux:

""" ) H.append(html_sco_header.sco_footer()) return "\n".join(H) def get_sorted_etuds_notes( evaluation: Evaluation, etudids: list, formsemestre_id: int ) -> list[dict]: """Liste d'infos sur les notes existantes pour les étudiants indiqués""" notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation.id) cnx = ndb.GetDBConnexion() etuds = [] for etudid in etudids: # infos identite etudiant e = sco_etud.etudident_list(cnx, {"etudid": etudid})[0] etud = Identite.get_etud(etudid) # TODO: refactor et eliminer etudident_list. e["etud"] = etud # utilisé seulement pour le tri -- a refactorer sco_etud.format_etud_ident(e) etuds.append(e) # infos inscription dans ce semestre e["inscr"] = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( {"etudid": etudid, "formsemestre_id": formsemestre_id} )[0] # Groupes auxquels appartient cet étudiant: e["groups"] = sco_groups.get_etud_groups(etudid, formsemestre_id) # Information sur absence warn_abs_lst: str = "" if evaluation.date_debut is not None and evaluation.date_fin is not None: assiduites_etud: Query = etud.assiduites.filter( Assiduite.etat == scu.EtatAssiduite.ABSENT, Assiduite.date_debut <= evaluation.date_fin, Assiduite.date_fin >= evaluation.date_debut, ) premiere_assi: Assiduite = assiduites_etud.first() if premiere_assi is not None: warn_abs_lst: str = ( f"absent {'justifié' if premiere_assi.est_just else ''}" ) e["absinfo"] = '' + warn_abs_lst + " " # Note actuelle de l'étudiant: if etudid in notes_db: e["val"] = scu.fmt_note( notes_db[etudid]["value"], fixed_precision_str=False ) user = ( User.query.get(notes_db[etudid]["uid"]) if notes_db[etudid]["uid"] else None ) e["explanation"] = ( f"""{ notes_db[etudid]["date"].strftime("%d/%m/%y %Hh%M") } par {user.get_nomplogin() if user else '?' } {(' : ' + notes_db[etudid]["comment"]) if notes_db[etudid]["comment"] else ''} """ ) 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["etud"].sort_key) return etuds def _form_saisie_notes( evaluation: Evaluation, modimpl: ModuleImpl, groups_infos, destination="" ): """Formulaire HTML saisie des notes dans l'évaluation du moduleimpl 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. """ formsemestre_id = modimpl.formsemestre_id formsemestre: FormSemestre = evaluation.moduleimpl.formsemestre res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) etudids = [ x[0] for x in sco_groups.do_evaluation_listeetuds_groups( evaluation.id, getallstudents=True, include_demdef=True ) ] if not etudids: return '
Aucun étudiant sélectionné !
' # Décisions de jury existantes ? # en BUT on ne considère pas les RCUEs car ils peuvenut avoir été validés depuis # d'autres semestres (les validations de RCUE n'indiquent pas si elles sont "externes") decisions_jury = { etudid: res.etud_has_decision(etudid, include_rcues=False) for etudid in etudids } # Nb de décisions de jury (pour les inscrits à l'évaluation): nb_decisions = sum(decisions_jury.values()) etuds = get_sorted_etuds_notes(evaluation, 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": groups_infos.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 modimpl.module.module_type in ( ModuleType.STANDARD, ModuleType.RESSOURCE, ModuleType.SAE, ): descr.append( ( "s3", { "input_type": "text", # affiche le barème "title": "Notes ", "cssclass": "formnote_bareme", "readonly": True, "default": f" / {evaluation.note_max:g}", }, ) ) elif modimpl.module.module_type == ModuleType.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(f"invalid module type ({modimpl.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 = f'disabled="{disabled}"' else: classdem = "" disabled_attr = "" # attribue a chaque element une classe css par groupe: for group_info in e["groups"]: etud_classes.append("group-" + str(group_info["group_id"])) label = f"""{e["civilite_str"]} { scu.format_nomprenom(e, reverse=True)}""" # Historique des saisies de notes: explanation = ( "" if disabled else f"""{ get_note_history_menu(evaluation.id, etudid) }""" ) explanation = e["absinfo"] + explanation # Lien modif decision de jury: explanation += f'' # Valeur actuelle du champ: initvalues["note_" + str(etudid)] = e["val"] label_link = f'{label}' # Element de formulaire: descr.append( ( "note_" + str(etudid), { "size": 5, "title": label_link, "explanation": explanation, "return_focus_next": True, "attributes": [ f'class="note{classdem}"', disabled_attr, f'''data-last-saved-value="{e['val']}"''', f'''data-orig-value="{e["val"]}"''', f'data-etudid="{etudid}"', ], "template": """%(label)s %(elem)s """, }, ) ) # H = [] if nb_decisions > 0: H.append( f"""
""" ) tf = TF( destination, scu.get_request_args(), descr, initvalues=initvalues, submitbutton=False, formid="formnotes", method="GET", ) H.append(tf.getform()) # check and init H.append( f"""Terminer """ ) if tf.canceled(): return None elif (not tf.submitted()) or not tf.result: # ajout formulaire saisie notes manquantes H.append( f"""
Mettre les notes manquantes à ABS indique "absent" (zéro), EXC "excusé" (neutralisées), ATT "attente"
""" ) # affiche formulaire return "\n".join(H) else: # form submission # rien à faire return None def save_notes( evaluation: Evaluation, notes: list[tuple[(int, float)]], comment: str = "" ) -> dict: """Enregistre une liste de notes. Vérifie que les étudiants sont bien inscrits à ce module, et que l'on a le droit. Result: dict avec """ log(f"save_note: evaluation_id={evaluation.id} uid={current_user} notes={notes}") status_url = url_for( "notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=evaluation.moduleimpl_id, _external=True, ) # Check access: admin, respformation, or responsable_id if not evaluation.moduleimpl.can_edit_notes(current_user): return json_error(403, "modification notes non autorisee pour cet utilisateur") # valid_notes, _, _, _, _ = check_notes(notes, evaluation) if valid_notes: etudids_changed, _, etudids_with_decision, messages = notes_add( current_user, evaluation.id, valid_notes, comment=comment, do_it=True ) ScolarNews.add( typ=ScolarNews.NEWS_NOTE, obj=evaluation.moduleimpl_id, text=f"""Chargement notes dans { evaluation.moduleimpl.module.titre or evaluation.moduleimpl.module.code}""", url=status_url, max_frequency=30 * 60, # 30 minutes ) result = { "etudids_with_decision": etudids_with_decision, "etudids_changed": etudids_changed, "history_menu": { etudid: get_note_history_menu(evaluation.id, etudid) for etudid in etudids_changed }, "messages": messages, } else: result = { "etudids_changed": [], "etudids_with_decision": [], "history_menu": [], "messages": [], } return result def get_note_history_menu(evaluation_id: int, etudid: int) -> str: """Menu HTML historique de la note""" history = sco_undo_notes.get_note_history(evaluation_id, etudid) if not history: return "" H = [] if len(history) > 1: H.append( f'