From 9b825c0fb10a1739da75f7a7bf2dcda0ec8380e4 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 14 Jul 2024 22:20:37 +0200 Subject: [PATCH] =?UTF-8?q?Saisie=20notes=20multi-=C3=A9valuations.=20Clos?= =?UTF-8?q?es=20#942.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/evaluations.py | 10 +- app/scodoc/sco_excel.py | 51 +- app/scodoc/sco_saisie_excel.py | 450 ++++++++++++------ app/scodoc/sco_saisie_notes.py | 62 ++- app/static/css/scodoc.css | 13 + app/templates/formsemestre/import_notes.j2 | 34 +- .../formsemestre/import_notes_after.j2 | 54 +++ app/views/notes.py | 8 +- sco_version.py | 2 +- 9 files changed, 486 insertions(+), 198 deletions(-) create mode 100644 app/templates/formsemestre/import_notes_after.j2 diff --git a/app/models/evaluations.py b/app/models/evaluations.py index 4af3ab847..d2607f0da 100644 --- a/app/models/evaluations.py +++ b/app/models/evaluations.py @@ -267,10 +267,12 @@ class Evaluation(models.ScoDocModel): @classmethod def get_evaluation( - cls, evaluation_id: int | str, dept_id: int = None + cls, evaluation_id: int | str, dept_id: int = None, accept_none=False ) -> "Evaluation": - """Evaluation ou 404, cherche uniquement dans le département spécifié ou le courant.""" - from app.models import FormSemestre, ModuleImpl + """Evaluation ou 404, cherche uniquement dans le département spécifié ou le courant. + Si accept_none, return None si l'id est invalide ou n'existe pas. + """ + from app.models import FormSemestre if not isinstance(evaluation_id, int): try: @@ -282,6 +284,8 @@ class Evaluation(models.ScoDocModel): query = cls.query.filter_by(id=evaluation_id) if dept_id is not None: query = query.join(ModuleImpl).join(FormSemestre).filter_by(dept_id=dept_id) + if accept_none: + return query.first() return query.first_or_404() @classmethod diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py index f82a04f98..da6050936 100644 --- a/app/scodoc/sco_excel.py +++ b/app/scodoc/sco_excel.py @@ -60,12 +60,12 @@ class COLORS(Enum): LIGHT_YELLOW = "FFFFFF99" -# Un style est enregistré comme un dictionnaire qui précise la valeur d'un attribut dans la liste suivante: +# Un style est enregistré comme un dictionnaire avec des attributs dans la liste suivante: # font, border, number_format, fill,... # (cf https://openpyxl.readthedocs.io/en/stable/styles.html#working-with-styles) -def xldate_as_datetime(xldate, datemode=0): +def xldate_as_datetime(xldate): """Conversion d'une date Excel en datetime python Deux formats de chaîne acceptés: * JJ/MM/YYYY (chaîne naïve) @@ -187,8 +187,8 @@ def excel_make_style( class ScoExcelSheet: """Représente une feuille qui peut être indépendante ou intégrée dans un ScoExcelBook. - En application des directives de la bibliothèque sur l'écriture optimisée, l'ordre des opérations - est imposé: + En application des directives de la bibliothèque sur l'écriture optimisée, + l'ordre des opérations est imposé: * instructions globales (largeur/maquage des colonnes et ligne, ...) * construction et ajout des cellules et ligne selon le sens de lecture (occidental) ligne de haut en bas et cellules de gauche à droite (i.e. A1, A2, .. B1, B2, ..) @@ -199,7 +199,7 @@ class ScoExcelSheet: """Création de la feuille. sheet_name -- le nom de la feuille default_style -- le style par défaut des cellules ws - -- None si la feuille est autonome (dans ce cas elle crée son propre wb), sinon c'est la worksheet + -- None si la feuille est autonome (elle crée son propre wb), sinon c'est la worksheet créée par le workbook propriétaire un workbook est créé et associé à cette feuille. """ # Le nom de la feuille ne peut faire plus de 31 caractères. @@ -228,7 +228,8 @@ class ScoExcelSheet: fill=None, number_format=None, font=None, - ): + ) -> dict: + "création d'un dict" style = {} if font is not None: style["font"] = font @@ -393,7 +394,7 @@ class ScoExcelSheet: if isinstance(value, datetime.date): cell.data_type = "d" cell.number_format = FORMAT_DATE_DDMMYY - elif isinstance(value, int) or isinstance(value, float): + elif isinstance(value, (int, float)): cell.data_type = "n" else: cell.data_type = "s" @@ -432,10 +433,11 @@ class ScoExcelSheet: Ce flux pourra ensuite être repris dans send_excel_file (classeur mono feille) ou pour la génération d'un classeur multi-feuilles """ - for row in self.column_dimensions.keys(): - self.ws.column_dimensions[row] = self.column_dimensions[row] - for row in self.row_dimensions.keys(): - self.ws.row_dimensions[row] = self.row_dimensions[row] + for k, v in self.column_dimensions.items(): + self.ws.column_dimensions[k] = v + + for k, v in self.row_dimensions.items(): + self.ws.row_dimensions[k] = self.row_dimensions[v] for row in self.rows: self.ws.append(row) @@ -529,17 +531,6 @@ def excel_file_to_list(filename): ) from exc -def excel_workbook_to_list(filename): - try: - return _excel_workbook_to_list(filename) - except Exception as exc: - raise ScoValueError( - """Le fichier xlsx attendu n'est pas lisible ! - Peut-être avez-vous fourni un fichier au mauvais format (txt, xls, ...) - """ - ) from exc - - def _open_workbook(filelike, dump_debug=False) -> Workbook: """Open document. On error, if dump-debug is True, dump data in /tmp for debugging purpose @@ -559,7 +550,7 @@ def _open_workbook(filelike, dump_debug=False) -> Workbook: return workbook -def _excel_to_list(filelike): +def _excel_to_list(filelike) -> tuple[list, list[list]]: """returns list of list""" workbook = _open_workbook(filelike) diag = [] # liste de chaines pour former message d'erreur @@ -576,7 +567,7 @@ def _excel_to_list(filelike): return diag, matrix -def _excel_sheet_to_list(sheet: Worksheet, sheet_name: str) -> tuple[list, list]: +def _excel_sheet_to_list(sheet: Worksheet, sheet_name: str) -> tuple[list, list[list]]: """read a spreadsheet sheet, and returns: - diag : a list of strings (error messages aimed at helping the user) - a list of lists: the spreadsheet cells @@ -609,14 +600,21 @@ def _excel_sheet_to_list(sheet: Worksheet, sheet_name: str) -> tuple[list, list] return diag, matrix -def _excel_workbook_to_list(filelike): +def excel_workbook_to_list(filelike): """Lit un classeur (workbook): chaque feuille est lue et est convertie en une liste de listes. Returns: - diag : a list of strings (error messages aimed at helping the user) - a list of lists: the spreadsheet cells """ - workbook = _open_workbook(filelike) + try: + workbook = _open_workbook(filelike) + except Exception as exc: + raise ScoValueError( + """Le fichier xlsx attendu n'est pas lisible ! + Peut-être avez-vous fourni un fichier au mauvais format (txt, xls, ...) + """ + ) from exc diag = [] # liste de chaines pour former message d'erreur if len(workbook.sheetnames) < 1: diag.append("Aucune feuille trouvée dans le classeur !") @@ -631,6 +629,7 @@ def _excel_workbook_to_list(filelike): return diag, matrix_list +# TODO déplacer dans un autre fichier def excel_feuille_listeappel( sem, groupname, diff --git a/app/scodoc/sco_saisie_excel.py b/app/scodoc/sco_saisie_excel.py index 620111dc3..1ef7e858a 100644 --- a/app/scodoc/sco_saisie_excel.py +++ b/app/scodoc/sco_saisie_excel.py @@ -23,6 +23,21 @@ ############################################################################## """Fichier excel de saisie des notes + + +## Notes d'une évaluation + +saisie_notes_tableur (formulaire) + -> feuille_saisie_notes (génération de l'excel) + -> do_evaluations_upload_xls + +## Notes d'un semestre + +formsemestre_import_notes (formulaire, import_notes.j2) + -> feuille_import_notes (génération de l'excel) + -> formsemestre_import_notes + + """ from collections import defaultdict from typing import AnyStr @@ -30,13 +45,14 @@ from typing import AnyStr from openpyxl.styles import Alignment, Border, Color, Font, PatternFill, Side from openpyxl.styles.numbers import FORMAT_GENERAL -from flask import g, request, url_for +from flask import g, render_template, request, url_for from flask_login import current_user -from app.models import Evaluation, FormSemestre, Identite, Module, ScolarNews +from app.models import Evaluation, FormSemestre, Identite, ScolarNews from app.scodoc.sco_excel import COLORS, ScoExcelSheet from app.scodoc import ( html_sco_header, + sco_cache, sco_evaluations, sco_evaluation_db, sco_excel, @@ -48,6 +64,7 @@ from app.scodoc import ( from app.scodoc.sco_exceptions import AccessDenied, InvalidNoteValue import app.scodoc.sco_utils as scu from app.scodoc.TrivialFormulator import TrivialFormulator +from app.views import ScoData FONT_NAME = "Arial" @@ -180,7 +197,7 @@ def _insert_line_titles( ws.append_row(cells) # Calcul largeur colonnes (actuellement pour feuille import multi seulement) - # Le facteur prend en compte la tailel du font (14) + # Le facteur prend en compte la taille du font (14) font_size_factor = 1.25 column_widths = { ScoExcelSheet.i2col(idx): (len(str(cell.value)) + 2.0) * font_size_factor @@ -525,124 +542,89 @@ def generate_excel_import_notes( return ws.generate(column_widths=column_widths) -def do_evaluation_upload_xls() -> tuple[bool, str]: +def do_evaluations_upload_xls( + notefile, + comment: str = "", + evaluation: Evaluation | None = None, + formsemestre: FormSemestre | None = None, +) -> tuple[bool, str]: """ Soumission d'un fichier XLS (evaluation_id, notefile) + soit dans le formsemestre (import multi-eval) + soit dans une seule évaluation return: ok: bool - msg: message diagonistic à affciher + msg: message diagnostic à affciher """ - args = scu.get_request_args() - comment = args["comment"] - evaluation = Evaluation.get_evaluation(args["evaluation_id"]) - - # Check access (admin, respformation, responsable_id, ens) - if not evaluation.moduleimpl.can_edit_notes(current_user): - raise AccessDenied(f"Modification des notes impossible pour {current_user}") - # - diag, rows = sco_excel.excel_file_to_list(args["notefile"]) + diag, rows = sco_excel.excel_file_to_list(notefile) try: if not rows: raise InvalidNoteValue() + # Lecture des évaluations ids row_title_idx, evaluations, evaluations_col_idx = _get_sheet_evaluations( - rows, evaluation=evaluation, diag=diag + rows, evaluation=evaluation, formsemestre=formsemestre, diag=diag ) - # --- get notes -> list (etudid, value) - # ignore toutes les lignes ne commençant pas par ! - notes_by_eval = defaultdict( - list - ) # { evaluation_id : [ (etudid, note_value), ... ] } - ni = row_title_idx + 1 - for row in rows[row_title_idx + 1 :]: - if row: - cell0 = row[0].strip() - if cell0 and cell0[0] == "!": - etudid = cell0[1:] - # check etud - etud = Identite.get_etud(etudid, accept_none=True) - if not etud: - diag.append( - f"étudiant id invalide en ligne {ni+1}" - ) # ligne excel à partir de 1 - else: - _read_notes_evaluations( - row, - etud, - evaluations, - notes_by_eval, - evaluations_col_idx, - ) - ni += 1 - # -- Check values de chaque évaluation - valid_notes_by_eval, etudids_without_notes_by_eval, etudids_absents_by_eval = ( - _check_notes_evaluations(evaluations, notes_by_eval, diag) + # Vérification des permissions (admin, resp. formation, responsable_id, ens) + for e in evaluations: + if not e.moduleimpl.can_edit_notes(current_user): + raise AccessDenied( + f"""Modification des notes + dans le module {e.moduleimpl.module.code} + impossible pour {current_user}""" + ) + + # Lecture des notes + notes_by_eval = _read_notes_from_rows( + rows, diag, evaluations, evaluations_col_idx, start=row_title_idx + 1 ) # -- Enregistre les notes de chaque évaluation - messages_by_eval: dict[int, str] = {} - etudids_with_decisions = set() - for evaluation in evaluations: - valid_notes = valid_notes_by_eval.get(evaluation.id) - if not valid_notes: - continue - etudids_changed, nb_suppress, etudids_with_decisions_eval, messages = ( - sco_saisie_notes.notes_add( - current_user, - evaluation.id, - valid_notes_by_eval[evaluation.id], - comment, - ) + with sco_cache.DeferredSemCacheManager(): + messages_by_eval, etudids_with_decisions = _record_notes_evaluations( + evaluations, notes_by_eval, comment, diag, rows=rows ) - etudids_with_decisions |= set(etudids_with_decisions_eval) - msg = f"""
- - """ - if messages: - msg += f"""
Attention : -
    -
  • { - '
  • '.join(messages) - } -
  • -
-
""" - msg += """
""" - messages_by_eval[evaluation.id] = msg # -- News - module: Module = evaluation.moduleimpl.module - status_url = url_for( - "notes.moduleimpl_status", - scodoc_dept=g.scodoc_dept, - moduleimpl_id=evaluation.moduleimpl_id, - _external=True, - ) + if len(evaluations) > 1: + modules_str = ", ".join( + [evaluation.moduleimpl.module.code for evaluation in evaluations] + ) + status_url = url_for( + "notes.formsemestre_status", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre.id, + ) + obj_id = formsemestre.id + else: + modules_str = ( + evaluation.moduleimpl.module.titre or evaluation.moduleimpl.module.code + ) + status_url = url_for( + "notes.moduleimpl_status", + scodoc_dept=g.scodoc_dept, + moduleimpl_id=evaluation.moduleimpl_id, + ) + obj_id = evaluation.moduleimpl_id ScolarNews.add( typ=ScolarNews.NEWS_NOTE, - obj=evaluation.moduleimpl_id, - text=f"""Chargement notes dans { - module.titre or module.code}""", + obj=obj_id, + text=f"""Chargement notes dans {modules_str}""", url=status_url, max_frequency=30 * 60, # 30 minutes ) + msg = "
" + "\n".join(messages_by_eval.values()) + "
" if etudids_with_decisions: - msg += """

Important: il y avait déjà des décisions de jury - enregistrées, qui sont à revoir suite à cette modification !

- """ + msg = ( + """

Important: + Il y avait déjà des décisions de jury + enregistrées, qui sont à revoir suite à cette modification ! +

+ """ + + msg + ) return True, msg except InvalidNoteValue: @@ -657,10 +639,101 @@ def do_evaluation_upload_xls() -> tuple[bool, str]: return False, msg + "

(pas de notes modifiées)

" +def _read_notes_from_rows( + rows: list[list], diag, evaluations, evaluations_col_idx, start=0 +): + """--- get notes -> list (etudid, value) + ignore toutes les lignes ne commençant pas par '!' + """ + # { evaluation_id : [ (etudid, note_value), ... ] } + notes_by_eval = defaultdict(list) + ni = start + for row in rows[start:]: + if row: + cell0 = row[0].strip() + if cell0 and cell0[0] == "!": + etudid = cell0[1:] + # check etud + etud = Identite.get_etud(etudid, accept_none=True) + if not etud: + diag.append( + f"étudiant id invalide en ligne {ni+1}" + ) # ligne excel à partir de 1 + else: + _read_notes_evaluations( + row, + etud, + evaluations, + notes_by_eval, + evaluations_col_idx, + ) + ni += 1 + + return notes_by_eval + + +def _record_notes_evaluations( + evaluations, notes_by_eval, comment, diag, rows: list[list[str]] | None = None +) -> tuple[dict[int, str], set[int]]: + """Enregistre les notes dans les évaluations + Return: messages_by_eval, etudids_with_decisions + """ + # -- Check values de chaque évaluation + valid_notes_by_eval, etudids_without_notes_by_eval, etudids_absents_by_eval = ( + _check_notes_evaluations(evaluations, notes_by_eval, diag, rows=rows) + ) + + messages_by_eval: dict[int, str] = {} + etudids_with_decisions = set() + for evaluation in evaluations: + valid_notes = valid_notes_by_eval.get(evaluation.id) + if not valid_notes: + continue + etudids_changed, nb_suppress, etudids_with_decisions_eval, messages = ( + sco_saisie_notes.notes_add( + current_user, evaluation.id, valid_notes, comment + ) + ) + etudids_with_decisions |= set(etudids_with_decisions_eval) + msg = f"""
+
" + ) + msg += "" + if messages: + msg += f"""
Attention : + +
""" + msg += """""" + messages_by_eval[evaluation.id] = msg + return messages_by_eval, etudids_with_decisions + + def _check_notes_evaluations( evaluations: list[Evaluation], notes_by_eval: dict[int, list[tuple[int, str]]], diag: list[str], + rows: list[list[str]] | None = None, ) -> tuple[dict[int, list[tuple[int, str]]], list[int], list[int]]: """Vérifie que les notes pour ces évaluations sont valides. Raise InvalidNoteValue et rempli diag si ce n'est pas le cas. @@ -678,29 +751,40 @@ def _check_notes_evaluations( etudids_non_inscrits, ) = sco_saisie_notes.check_notes(notes_by_eval[evaluation.id], evaluation) if invalids: - diag.append( - f"Erreur: la feuille contient {len(invalids)} notes invalides

" - ) + diag.append(f"Erreur: la feuille contient {len(invalids)} notes invalides") + msg = f"""Notes invalides dans { + evaluation.moduleimpl.module.code} {evaluation.description} pour : """ if len(invalids) < 25: etudsnames = [ Identite.get_etud(etudid).nom_prenom() for etudid in invalids ] - diag.append("Notes invalides pour: " + ", ".join(etudsnames)) + msg += ", ".join(etudsnames) else: - diag.append("Notes invalides pour plus de 25 étudiants") + msg += "plus de 25 étudiants" + diag.append(msg) raise InvalidNoteValue() if etudids_non_inscrits: + msg = "" + if len(etudids_non_inscrits) < 25: + # retrouve numéro ligne et données invalides dans fichier + for etudid in etudids_non_inscrits: + try: + index = [row[0] for row in rows].index(f"!{etudid}") + except ValueError: + index = None + msg += f"""
  • Ligne {index+1}: + {rows[index][1]} {rows[index][2]} (id={rows[index][0]}) +
  • """ + else: + msg += "
  • sur plus de 25 lignes
  • " diag.append( f"""Erreur: la feuille contient {len(etudids_non_inscrits) - } étudiants non inscrits

    """ + } étudiants inexistants ou non inscrits à l'évaluation + {evaluation.moduleimpl.module.code} + {evaluation.description} + + """ ) - if len(etudids_non_inscrits) < 25: - diag.append( - "etudid invalides (inexistants ou non inscrits): " - + ", ".join(str(etudid) for etudid in etudids_non_inscrits) - ) - else: - diag.append("etudid invalides sur plus de 25 lignes") raise InvalidNoteValue() return valid_notes_by_eval, etudids_without_notes_by_eval, etudids_absents_by_eval @@ -724,8 +808,61 @@ def _read_notes_evaluations( notes_by_eval[evaluation.id].append((etud.id, val)) +def _xls_search_sheet_code( + rows: list[list[str]], diag: list[str] = None +) -> tuple[int, int | dict[int, int]]: + """Cherche dans la feuille (liste de listes de chaines) + la ligne identifiant la ou les évaluations. + Si MULTIEVAL (import de plusieurs), renvoie + - l'indice de la ligne des TITRES + - un dict evaluations_col_idx: { evaluation_id : indice de sa colonne dans la feuille } + Si une seule éval (chargement dans une évaluation) + - l'indice de la ligne des TITRES + - l'evaluation_id indiqué dans la feuille + """ + for i, row in enumerate(rows): + if not row: + diag.append("Erreur: feuille invalide (ligne vide ?)") + raise InvalidNoteValue() + eval_code = row[0].strip() + # -- eval code: first cell in 1st column beginning by "!" + if eval_code.startswith("!"): # code évaluation trouvé + try: + sheet_eval_id = int(eval_code[1:]) + except ValueError as exc: + diag.append("Erreur: feuille invalide ! (code évaluation invalide)") + raise InvalidNoteValue() from exc + return i, sheet_eval_id + + # -- Muti-évaluation: les codes sont au dessus des titres + elif eval_code == "MULTIEVAL": # feuille import multi-eval + # cherche les ids des évaluations sur la même ligne + try: + evaluation_ids = [int(x) for x in row[4:]] + except ValueError as exc: + diag.append( + f"Erreur: feuille invalide ! (code évaluation invalide sur ligne {i+1})" + ) + raise InvalidNoteValue() from exc + + evaluations_col_idx = { + evaluation_id: j + for (j, evaluation_id) in enumerate(evaluation_ids, start=4) + } + return ( + i + 1, # i+1 car MULTIEVAL sur la ligne précédent les titres + evaluations_col_idx, + ) + + diag.append("Erreur: feuille invalide ! (pas de ligne code évaluation)") + raise InvalidNoteValue() + + def _get_sheet_evaluations( - rows: list[list[str]], evaluation: Evaluation | None = None, diag: list[str] = None + rows: list[list[str]], + evaluation: Evaluation | None = None, + formsemestre: FormSemestre | None = None, + diag: list[str] = None, ) -> tuple[int, list[Evaluation], dict[int, int]]: """ rows: les valeurs (str) des cellules de la feuille @@ -735,35 +872,38 @@ def _get_sheet_evaluations( formsemestre ou evaluation doivent être indiqués. Résultat: - row_title_idx: l'indice (à partir de 0) de la ligne titre (après laquelle commencent les notes) + row_title_idx: l'indice (à partir de 0) de la ligne TITRE (après laquelle commencent les notes) evaluations: liste des évaluations à remplir evaluations_col_idx: { evaluation_id : indice de sa colonne dans la feuille } """ - # -- search eval code: first cell in 1st column beginning by "!" - eval_code = None - for i, row in enumerate(rows): - if not row: - diag.append("Erreur: format invalide (ligne vide ?)") + + i, r = _xls_search_sheet_code(rows, diag) + if isinstance(r, int): # mono-eval + sheet_eval_id = r + if sheet_eval_id != evaluation.id: + diag.append( + f"""Erreur: fichier invalide: le code d'évaluation de correspond pas ! ('{ + sheet_eval_id or ('non trouvé')}' != '{evaluation.id}')""" + ) raise InvalidNoteValue() - eval_code = row[0].strip() - if eval_code.startswith("!"): - break - if not eval_code: - diag.append("Erreur: format invalide ! (pas de ligne evaluation_id)") - raise InvalidNoteValue() - - try: - sheet_eval_id = int(eval_code[1:]) - except ValueError: - sheet_eval_id = None - if sheet_eval_id != evaluation.id: - diag.append( - f"""Erreur: fichier invalide: le code d'évaluation de correspond pas ! ('{ - sheet_eval_id or ('non trouvé')}' != '{evaluation.id}')""" - ) - raise InvalidNoteValue() - - return i, [evaluation], {evaluation.id: 4} + return i, [evaluation], {evaluation.id: 4} + if isinstance(r, dict): # multi-eval + evaluations = [] + evaluations_col_idx = r + # Load and check evaluations + for evaluation_id in evaluations_col_idx: + evaluation = Evaluation.get_evaluation(evaluation_id, accept_none=True) + if evaluation is None: + diag.append(f"""Erreur: l'évaluation {evaluation_id} n'existe pas""") + raise InvalidNoteValue() + if evaluation.moduleimpl.formsemestre_id != formsemestre.id: + diag.append( + f"""Erreur: l'évaluation {evaluation_id} n'existe pas dans ce semestre""" + ) + raise InvalidNoteValue() + evaluations.append(evaluation) + return i, evaluations, evaluations_col_idx + raise ValueError("_get_sheet_evaluations") def saisie_notes_tableur(evaluation_id: int, group_ids=()): @@ -840,7 +980,7 @@ def saisie_notes_tableur(evaluation_id: int, group_ids=()): Étape 2 : chargement d'un fichier de notes""" # ' ) - nf = TrivialFormulator( + tf = TrivialFormulator( request.base_url, scu.get_request_args(), ( @@ -861,23 +1001,27 @@ def saisie_notes_tableur(evaluation_id: int, group_ids=()): formid="notesfile", submitlabel="Télécharger", ) - if nf[0] == 0: + if tf[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(tf[1]) + elif tf[0] == -1: H.append("

    Annulation

    ") - elif nf[0] == 1: - updiag = do_evaluation_upload_xls() - if updiag[0]: + elif tf[0] == 1: + args = scu.get_request_args() + evaluation = Evaluation.get_evaluation(args["evaluation_id"]) + ok, diagnostic_msg = do_evaluations_upload_xls( + args["notefile"], evaluation=evaluation, comment=args["comment"] + ) + if ok: H.append( f"""
    Notes chargées !
    - {updiag[1]} + {diagnostic_msg}
    Notes non chargées !

    - {updiag[1]} + {diagnostic_msg}

    @@ -955,7 +1099,8 @@ def saisie_notes_tableur(evaluation_id: int, group_ids=()): Remarques:

    -
    Une fois que le fichier tableur exporté ci-dessus est rempli, téléchargez-le +
    Une fois que le fichier tableur exporté ci-dessus est rempli, téléchargez-le ci-dessous. Le texte "commentaire" sera associé à chaque note pour l'historique, il n'est jamais montré aux étudiants.
    @@ -47,4 +49,32 @@ Le texte "commentaire" sera associé à chaque note pour l'historique, il n'est
    +
    +À l'étape 2, indiquer le fichier Excel + téléchargé à l'étape 1 et dans lequel on a saisi des notes. +
    + Remarques : +
      +
    • Le fichier Excel doit impérativement être celui chargé à + l'étape 1 pour ce semestre. Il n'est pas possible d'utiliser + une liste d'appel ou autre document Excel téléchargé d'une autre page. +
    • +
    • Ne pas supprimer les lignes et colonnes cachées, qui + contiennent des codes. Le fichier exporté contient toutles les + évaluations du semestre, vous pouvez au besoin supprimer certaines + colonnes d'évaluations. +
    • + +
    • Le fichier peut être incomplet: on + peut ne saisir que quelques notes et répéter l'opération plus tard (en + téléchargeant un nouveau fichier ou en passant par le formulaire de + saisie). +
    • +
    • Seules les valeurs des notes modifiées sont prises en + compte. +
    • +
    +
    +
    + {% endblock %} diff --git a/app/templates/formsemestre/import_notes_after.j2 b/app/templates/formsemestre/import_notes_after.j2 new file mode 100644 index 000000000..5f7222c81 --- /dev/null +++ b/app/templates/formsemestre/import_notes_after.j2 @@ -0,0 +1,54 @@ +{% extends "sco_page.j2" %} +{% import 'wtf.j2' as wtf %} + +{% block styles %} +{{super()}} + +{% endblock %} + +{% block app_content %} +

    Import de notes dans les évaluations du semestre

    + +
    +
    + {% if ok %} + Notes importées avec succès + {% else %} + Erreur: aucune note chargée + {% endif %} +
    +
    + {{ diagnostic_msg | safe }} +
    +
    + +
    + +
    + +{% endblock %} diff --git a/app/views/notes.py b/app/views/notes.py index 3234f0636..95c071a76 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -1870,15 +1870,17 @@ def formsemestre_import_notes(formsemestre_id: int): # Handle file upload and form processing notefile = form.notefile.data comment = form.comment.data - # Save the file and process form data here - raise ScoValueError("unimplemented") - return redirect(url_for("index")) + # + return sco_saisie_excel.formsemestre_import_notes( + formsemestre, notefile, comment + ) return render_template( "formsemestre/import_notes.j2", evaluations=formsemestre.get_evaluations(), form=form, formsemestre=formsemestre, + title="Importation des notes", sco=ScoData(formsemestre=formsemestre), ) diff --git a/sco_version.py b/sco_version.py index b8822c3e0..60714411b 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.992" +SCOVERSION = "9.7.0" SCONAME = "ScoDoc"