diff --git a/app/models/evaluations.py b/app/models/evaluations.py
index 98017e0e2..4af3ab847 100644
--- a/app/models/evaluations.py
+++ b/app/models/evaluations.py
@@ -363,6 +363,8 @@ class Evaluation(models.ScoDocModel):
return f"le {self.date_debut.strftime('%d/%m/%Y')} à {_h(self.date_debut)}"
if self.date_debut.date() == self.date_fin.date(): # même jour
if self.date_debut.time() == self.date_fin.time():
+ if self.date_fin.time() == datetime.time(0, 0):
+ return f"le {self.date_debut.strftime('%d/%m/%Y')}" # sans heure
return (
f"le {self.date_debut.strftime('%d/%m/%Y')} à {_h(self.date_debut)}"
)
diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py
index 08f1af9f5..1bae0cdd7 100644
--- a/app/scodoc/sco_excel.py
+++ b/app/scodoc/sco_excel.py
@@ -244,7 +244,10 @@ class ScoExcelSheet:
return style
@staticmethod
- def i2col(idx):
+ def i2col(idx: int | str) -> str:
+ "traduit un index ou lettre de colonne en lettre de colonne"
+ if isinstance(idx, str):
+ return idx
if idx < 26: # one letter key
return chr(idx + 65)
else: # two letters AA..ZZ
@@ -252,19 +255,26 @@ class ScoExcelSheet:
second = (idx % 26) + 65
return "" + chr(first) + chr(second)
- def set_column_dimension_width(self, cle=None, value=21):
- """Détermine la largeur d'une colonne. cle -- identifie la colonne ("A" "B", ... ou 0, 1, 2, ...) si None,
- value donne la liste des largeurs de colonnes depuis A, B, C, ... value -- la dimension (unité : 7 pixels
- comme affiché dans Excel)
+ def set_column_dimension_width(self, cle=None, value: int | str | list = 21):
+ """Détermine la largeur d'une colonne.
+ cle -- identifie la colonne (lettre ou indice à partir de 0),
+ Si cle is None, affecte toutes les colonnes.
+
+ value est soit la liste des largeurs de colonnes [ (0, width0), (1, width1), ...]
+ soit la largeur de la colonne indiquée par cle, soit "auto".
+ Largeurs en unité : 7 pixels comme affiché dans Excel)
+ ou value == "auto", ajuste la largeur au contenu
"""
if cle is None:
- for i, val in enumerate(value):
- self.ws.column_dimensions[self.i2col(i)].width = val
- # No keys: value is a list of widths
- elif isinstance(cle, str): # accepts set_column_with("D", ...)
- self.ws.column_dimensions[cle].width = value
+ cols_widths = enumerate(value)
else:
- self.ws.column_dimensions[self.i2col(cle)].width = value
+ cols_widths = [(cle, value)]
+
+ for idx, width in cols_widths:
+ if width == "auto":
+ self.adjust_column_widths(column_letter=self.i2col(idx))
+ else:
+ self.ws.column_dimensions[self.i2col(idx)].width = width
def set_row_dimension_height(self, cle=None, value=21):
"""Détermine la hauteur d'une ligne. cle -- identifie la ligne (1, 2, ...) si None,
@@ -291,10 +301,48 @@ class ScoExcelSheet:
"""
self.ws.column_dimensions[cle].hidden = value
- def set_auto_filter(self, range):
- self.auto_filter = range
+ def set_auto_filter(self, filter_range):
+ "met en place un auto-filter excel: le range désigne les cellules de titres"
+ self.auto_filter = filter_range
- def make_cell(self, value: any = None, style=None, comment=None):
+ def adjust_column_widths(
+ self, column_letter=None, min_row=None, max_row=None, min_col=None, max_col=None
+ ):
+ """Adjust columns widths to fit their content.
+ If column_letter, adjust only this column, else adjust all.
+ (min_row, max_row, min_col, max_col) can be used to restrinct the area to consider
+ while determining the widths.
+ """
+ # Create a dictionary to store the maximum width of each column
+ col_widths = {}
+
+ if column_letter is None:
+ # Iterate over each row and cell in the worksheet
+ for row in self.ws.iter_rows(
+ min_row=min_row, max_row=max_row, min_col=min_col, max_col=max_col
+ ):
+ for cell in row:
+ # Get the length of the cell value (converted to string)
+ cell_value = str(cell.value)
+ # Update the maximum width for the column
+ col_widths[cell.column_letter] = max(
+ col_widths.get(cell.column_letter, 0), len(cell_value)
+ )
+ else:
+ min_row = self.ws.min_row if min_row is None else min_row
+ max_row = self.ws.max_row if max_row is None else max_row
+ for row in range(min_row, max_row + 1):
+ cell = self.ws[f"{column_letter}{row}"]
+ cell_value = str(cell.value)
+ col_widths[cell.column_letter] = max(
+ col_widths.get(cell.column_letter, 0), len(cell_value)
+ )
+
+ # Set the column widths based on the maximum length found
+ for col, width in col_widths.items():
+ self.ws.column_dimensions[col].width = width
+
+ def make_cell(self, value: any = None, style: dict = None, comment=None):
"""Construit une cellule.
value -- contenu de la cellule (texte, numérique, booléen ou date)
style -- style par défaut (dictionnaire cf. excel_make_style) de la feuille si non spécifié
@@ -307,9 +355,8 @@ class ScoExcelSheet:
elif value is False:
value = 0
elif isinstance(value, datetime.datetime):
- value = value.replace(
- tzinfo=None
- ) # make date naive (cf https://openpyxl.readthedocs.io/en/latest/datetime.html#timezones)
+ # make date naive (cf https://openpyxl.readthedocs.io/en/latest/datetime.html#timezones)
+ value = value.replace(tzinfo=None)
# création de la cellule
cell = WriteOnlyCell(self.ws, value)
@@ -358,13 +405,14 @@ class ScoExcelSheet:
for value, comment in zip(values, comments)
]
- def append_single_cell_row(self, value: any, style=None):
+ def append_single_cell_row(self, value: any, style=None, prefix=None):
"""construit une ligne composée d'une seule cellule et l'ajoute à la feuille.
mêmes paramètres que make_cell:
value -- contenu de la cellule (texte ou numérique)
style -- style par défaut de la feuille si non spécifié
+ prefix -- cellules ajoutées au début de la ligne
"""
- self.append_row([self.make_cell(value, style)])
+ self.append_row((prefix or []) + [self.make_cell(value, style)])
def append_blank_row(self):
"""construit une ligne vide et l'ajoute à la feuille."""
@@ -386,14 +434,21 @@ class ScoExcelSheet:
for row in self.rows:
self.ws.append(row)
- def generate(self):
+ def generate(self, column_widths=None):
"""génération d'un classeur mono-feuille"""
- # this method makes sense only if it is a standalone worksheet (else call workbook.generate()
+ # this method makes sense for standalone worksheet (else call workbook.generate())
if self.wb is None: # embeded sheet
raise ScoValueError("can't generate a single sheet from a ScoWorkbook")
- # construction d'un flux (https://openpyxl.readthedocs.io/en/stable/tutorial.html#saving-as-a-stream)
+ # construction d'un flux
+ # https://openpyxl.readthedocs.io/en/stable/tutorial.html#saving-as-a-stream
self.prepare()
+
+ # largeur des colonnes
+ if column_widths:
+ for k, v in column_widths.items():
+ self.set_column_dimension_width(k, v)
+
if self.auto_filter is not None:
self.ws.auto_filter.ref = self.auto_filter
with NamedTemporaryFile() as tmp:
@@ -446,182 +501,6 @@ def excel_simple_table(
return ws.generate()
-def excel_feuille_saisie(evaluation: "Evaluation", titreannee, description, lines):
- """Genere feuille excel pour saisie des notes.
- E: evaluation (dict)
- lines: liste de tuples
- (etudid, nom, prenom, etat, groupe, val, explanation)
- """
- sheet_name = "Saisie notes"
- ws = ScoExcelSheet(sheet_name)
-
- # ajuste largeurs colonnes (unite inconnue, empirique)
- ws.set_column_dimension_width("A", 11.0 / 7) # codes
- # ws.set_column_dimension_hidden("A", True) # codes
- ws.set_column_dimension_width("B", 164.00 / 7) # noms
- ws.set_column_dimension_width("C", 109.0 / 7) # prenoms
- ws.set_column_dimension_width("D", 164.0 / 7) # groupes
- ws.set_column_dimension_width("E", 115.0 / 7) # notes
- ws.set_column_dimension_width("F", 355.0 / 7) # remarques
- ws.set_column_dimension_width("G", 72.0 / 7) # colonne NIP
- ws.set_column_dimension_hidden("G", True) # colonne NIP cachée
-
- # fontes
- font_base = Font(name="Arial", size=12)
- font_bold = Font(name="Arial", bold=True)
- font_italic = Font(name="Arial", size=12, italic=True, color=COLORS.RED.value)
- font_titre = Font(name="Arial", bold=True, size=14)
- font_purple = Font(name="Arial", color=COLORS.PURPLE.value)
- font_brown = Font(name="Arial", color=COLORS.BROWN.value)
- font_blue = Font(name="Arial", size=9, color=COLORS.BLUE.value)
-
- # bordures
- side_thin = Side(border_style="thin", color=COLORS.BLACK.value)
- border_top = Border(top=side_thin)
- border_right = Border(right=side_thin)
-
- # fonds
- fill_light_yellow = PatternFill(
- patternType="solid", fgColor=COLORS.LIGHT_YELLOW.value
- )
-
- # styles
- style = {"font": font_base}
- style_titres = {"font": font_titre}
- style_expl = {"font": font_italic}
-
- style_ro = { # cells read-only
- "font": font_purple,
- "border": border_right,
- }
- style_dem = {
- "font": font_brown,
- "border": border_top,
- }
- style_nom = { # style pour nom, prenom, groupe
- "font": font_base,
- "border": border_top,
- }
- style_notes = {
- "font": font_bold,
- "number_format": FORMAT_GENERAL,
- "fill": fill_light_yellow,
- "border": border_top,
- }
- style_comment = {
- "font": font_blue,
- "border": border_top,
- }
-
- # filtre
- filter_top = 8
- filter_bottom = 8 + len(lines)
- filter_left = "A"
- filter_right = "G"
- ws.set_auto_filter(f"${filter_left}${filter_top}:${filter_right}${filter_bottom}")
-
- # ligne de titres
- ws.append_single_cell_row(
- "Feuille saisie note (à enregistrer au format excel)", style_titres
- )
- # lignes d'instructions
- ws.append_single_cell_row(
- "Saisir les notes dans la colonne E (cases jaunes)", style_expl
- )
- ws.append_single_cell_row("Ne pas modifier les cases en mauve !", style_expl)
- # Nom du semestre
- ws.append_single_cell_row(scu.unescape_html(titreannee), style_titres)
- # description evaluation
- ws.append_single_cell_row(scu.unescape_html(description), style_titres)
- ws.append_single_cell_row(
- f"Evaluation {evaluation.descr_date()} (coef. {(evaluation.coefficient):g})",
- style,
- )
- # ligne blanche
- ws.append_blank_row()
- # code et titres colonnes
- ws.append_row(
- [
- ws.make_cell("!%s" % evaluation.id, style_ro),
- ws.make_cell("Nom", style_titres),
- ws.make_cell("Prénom", style_titres),
- ws.make_cell("Groupe", style_titres),
- ws.make_cell("Note sur %g" % (evaluation.note_max or 0.0), style_titres),
- ws.make_cell("Remarque", style_titres),
- ws.make_cell("NIP", style_titres),
- ]
- )
-
- # etudiants
- for line in lines:
- st = style_nom
- if line[3] != "I":
- st = style_dem
- if line[3] == "D": # demissionnaire
- s = "DEM"
- else:
- s = line[3] # etat autre
- else:
- s = line[4] # groupes TD/TP/...
- try:
- val = float(line[5])
- except ValueError:
- val = line[5]
- ws.append_row(
- [
- ws.make_cell("!" + line[0], style_ro), # code
- ws.make_cell(line[1], st),
- ws.make_cell(line[2], st),
- ws.make_cell(s, st),
- ws.make_cell(val, style_notes), # note
- ws.make_cell(line[6], style_comment), # comment
- ws.make_cell(line[7], style_ro), # NIP
- ]
- )
-
- # ligne blanche
- ws.append_blank_row()
-
- # explication en bas
- ws.append_row([None, ws.make_cell("Code notes", style_titres)])
- ws.append_row(
- [
- None,
- ws.make_cell("ABS", style_expl),
- ws.make_cell("absent (0)", style_expl),
- ]
- )
- ws.append_row(
- [
- None,
- ws.make_cell("EXC", style_expl),
- ws.make_cell("pas prise en compte", style_expl),
- ]
- )
- ws.append_row(
- [
- None,
- ws.make_cell("ATT", style_expl),
- ws.make_cell("en attente", style_expl),
- ]
- )
- ws.append_row(
- [
- None,
- ws.make_cell("SUPR", style_expl),
- ws.make_cell("pour supprimer note déjà entrée", style_expl),
- ]
- )
- ws.append_row(
- [
- None,
- ws.make_cell("", style_expl),
- ws.make_cell("cellule vide -> note non modifiée", style_expl),
- ]
- )
- return ws.generate()
-
-
def excel_bytes_to_list(bytes_content):
try:
filelike = io.BytesIO(bytes_content)
diff --git a/app/scodoc/sco_saisie_excel.py b/app/scodoc/sco_saisie_excel.py
new file mode 100644
index 000000000..4982a6ae3
--- /dev/null
+++ b/app/scodoc/sco_saisie_excel.py
@@ -0,0 +1,655 @@
+##############################################################################
+#
+# 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
+#
+##############################################################################
+
+"""Fichier excel de saisie des notes
+"""
+
+from openpyxl.styles import Font, Border, Side, Alignment, PatternFill
+from openpyxl.styles.numbers import FORMAT_GENERAL
+
+from flask import g, request, url_for
+from flask_login import current_user
+
+from app.models import Evaluation, Identite, Module, ScolarNews
+from app.scodoc.sco_excel import COLORS, ScoExcelSheet
+from app.scodoc import (
+ html_sco_header,
+ sco_evaluations,
+ sco_excel,
+ sco_groups,
+ sco_groups_view,
+ sco_saisie_notes,
+ sco_users,
+)
+from app.scodoc.sco_exceptions import AccessDenied, InvalidNoteValue
+import app.scodoc.sco_utils as scu
+from app.scodoc.TrivialFormulator import TrivialFormulator
+
+FONT_NAME = "Arial"
+
+
+def excel_feuille_saisie(
+ evaluation: "Evaluation", titreannee, description, rows: list[dict]
+):
+ """Genere feuille excel pour saisie des notes.
+ E: evaluation (dict)
+ lines: liste de tuples
+ (etudid, nom, prenom, etat, groupe, val, explanation)
+ """
+ sheet_name = "Saisie notes"
+ ws = ScoExcelSheet(sheet_name)
+
+ # fontes
+ font_base = Font(name=FONT_NAME, size=12)
+ font_bold = Font(name=FONT_NAME, bold=True)
+ font_italic = Font(name=FONT_NAME, size=12, italic=True, color=COLORS.RED.value)
+ font_titre = Font(name=FONT_NAME, bold=True, size=14)
+ font_purple = Font(name=FONT_NAME, color=COLORS.PURPLE.value)
+ font_brown = Font(name=FONT_NAME, color=COLORS.BROWN.value)
+ font_blue = Font(name=FONT_NAME, size=9, color=COLORS.BLUE.value)
+
+ # bordures
+ side_thin = Side(border_style="thin", color=COLORS.BLACK.value)
+ border_top = Border(top=side_thin)
+ border_right = Border(right=side_thin)
+
+ # fonds
+ fill_light_yellow = PatternFill(
+ patternType="solid", fgColor=COLORS.LIGHT_YELLOW.value
+ )
+
+ # styles
+ styles = {
+ "base": {"font": font_base},
+ "titres": {"font": font_titre},
+ "explanation": {"font": font_italic},
+ "read-only": { # cells read-only
+ "font": font_purple,
+ "border": border_right,
+ },
+ "dem": {
+ "font": font_brown,
+ "border": border_top,
+ },
+ "nom": { # style pour nom, prenom, groupe
+ "font": font_base,
+ "border": border_top,
+ },
+ "notes": {
+ "alignment": Alignment(horizontal="right"),
+ "font": font_bold,
+ "number_format": FORMAT_GENERAL,
+ "fill": fill_light_yellow,
+ "border": border_top,
+ },
+ "comment": {
+ "font": font_blue,
+ "border": border_top,
+ },
+ }
+
+ # filtre auto excel sur colonnes
+ filter_top = 8
+ filter_bottom = 8 + len(rows)
+ filter_left = "A" # important: le code etudid en col A doit être trié en même temps
+ filter_right = "G"
+ ws.set_auto_filter(f"${filter_left}${filter_top}:${filter_right}${filter_bottom}")
+
+ # ligne de titres (utilise prefix pour se placer à partir de la colonne B)
+ ws.append_single_cell_row(
+ "Feuille saisie note (à enregistrer au format excel)",
+ styles["titres"],
+ prefix=[""],
+ )
+ # lignes d'instructions
+ ws.append_single_cell_row(
+ "Saisir les notes dans la colonne E (cases jaunes)",
+ styles["explanation"],
+ prefix=[""],
+ )
+ ws.append_single_cell_row(
+ "Ne pas modifier les cases en mauve !", styles["explanation"], prefix=[""]
+ )
+ # Nom du semestre
+ ws.append_single_cell_row(
+ scu.unescape_html(titreannee), styles["titres"], prefix=[""]
+ )
+ # description evaluation
+ ws.append_single_cell_row(
+ scu.unescape_html(description), styles["titres"], prefix=[""]
+ )
+ ws.append_single_cell_row(
+ f"Evaluation {evaluation.descr_date()} (coef. {(evaluation.coefficient):g})",
+ styles["base"],
+ prefix=[""],
+ )
+ # ligne blanche
+ ws.append_blank_row()
+ # code et titres colonnes
+ ws.append_row(
+ [
+ ws.make_cell(f"!{evaluation.id}", styles["read-only"]),
+ ws.make_cell("Nom", styles["titres"]),
+ ws.make_cell("Prénom", styles["titres"]),
+ ws.make_cell("Groupe", styles["titres"]),
+ ws.make_cell(
+ f"Note sur {(evaluation.note_max or 0.0):g}", styles["titres"]
+ ),
+ ws.make_cell("Remarque", styles["titres"]),
+ ws.make_cell("NIP", styles["titres"]),
+ ]
+ )
+
+ # etudiants
+ for row in rows:
+ st = styles["nom"]
+ if row["etat"] != scu.INSCRIT:
+ st = styles["dem"]
+ if row["etat"] == scu.DEMISSION: # demissionnaire
+ groupe_ou_etat = "DEM"
+ else:
+ groupe_ou_etat = row["etat"] # etat autre
+ else:
+ groupe_ou_etat = row["groupes"] # groupes TD/TP/...
+ try:
+ note_str = float(row["note"]) # export numérique excel
+ except ValueError:
+ note_str = row["note"] # "ABS", ...
+ ws.append_row(
+ [
+ ws.make_cell("!" + row["etudid"], styles["read-only"]),
+ ws.make_cell(row["nom"], st),
+ ws.make_cell(row["prenom"], st),
+ ws.make_cell(groupe_ou_etat, st),
+ ws.make_cell(note_str, styles["notes"]), # note
+ ws.make_cell(row["explanation"], styles["comment"]), # comment
+ ws.make_cell(row["code_nip"], styles["read-only"]),
+ ]
+ )
+
+ # ligne blanche
+ ws.append_blank_row()
+
+ # explication en bas
+ _insert_bottom_help(ws, styles)
+ ws.set_column_dimension_hidden("A", True) # colonne etudid cachée
+ ws.set_column_dimension_hidden("G", True) # colonne NIP cachée
+
+ return ws.generate(
+ column_widths={
+ "A": 11.0 / 7, # codes
+ "B": 164.00 / 7, # noms
+ "C": 109.0 / 7, # prenoms
+ "D": "auto", # groupes
+ "E": 115.0 / 7, # notes
+ "F": 355.0 / 7, # remarques
+ "G": 72.0 / 7, # colonne NIP
+ }
+ )
+
+
+def _insert_bottom_help(ws, styles: dict):
+ ws.append_row([None, ws.make_cell("Code notes", styles["titres"])])
+ ws.append_row(
+ [
+ None,
+ ws.make_cell("ABS", styles["explanation"]),
+ ws.make_cell("absent (0)", styles["explanation"]),
+ ]
+ )
+ ws.append_row(
+ [
+ None,
+ ws.make_cell("EXC", styles["explanation"]),
+ ws.make_cell("pas prise en compte", styles["explanation"]),
+ ]
+ )
+ ws.append_row(
+ [
+ None,
+ ws.make_cell("ATT", styles["explanation"]),
+ ws.make_cell("en attente", styles["explanation"]),
+ ]
+ )
+ ws.append_row(
+ [
+ None,
+ ws.make_cell("SUPR", styles["explanation"]),
+ ws.make_cell("pour supprimer note déjà entrée", styles["explanation"]),
+ ]
+ )
+ ws.append_row(
+ [
+ None,
+ ws.make_cell("", styles["explanation"]),
+ ws.make_cell("cellule vide -> note non modifiée", styles["explanation"]),
+ ]
+ )
+
+
+def feuille_saisie_notes(evaluation_id, group_ids: list[int] = None):
+ """Vue: document Excel pour saisie notes dans l'évaluation et les groupes indiqués"""
+ evaluation = Evaluation.get_evaluation(evaluation_id)
+ group_ids = group_ids or []
+ modimpl = evaluation.moduleimpl
+ formsemestre = modimpl.formsemestre
+ mod_responsable = sco_users.user_info(modimpl.responsable_id)
+ if evaluation.date_debut:
+ indication_date = evaluation.date_debut.date().isoformat()
+ else:
+ indication_date = scu.sanitize_filename(evaluation.description)[:12]
+ eval_name = f"{evaluation.moduleimpl.module.code}-{indication_date}"
+
+ date_str = (
+ f"""du {evaluation.date_debut.strftime(scu.DATE_FMT)}"""
+ if evaluation.date_debut
+ else "(sans date)"
+ )
+ eval_titre = f"""{evaluation.description if evaluation.description else "évaluation"
+ } {date_str}"""
+
+ description = f"""{eval_titre} en {evaluation.moduleimpl.module.abbrev or ""} ({
+ evaluation.moduleimpl.module.code
+ }) resp. {mod_responsable["prenomnom"]}"""
+
+ groups_infos = sco_groups_view.DisplayedGroupsInfos(
+ group_ids=group_ids,
+ formsemestre_id=formsemestre.id,
+ select_all_when_unspecified=True,
+ etat=None,
+ )
+ groups = sco_groups.listgroups(groups_infos.group_ids)
+ gr_title_filename = sco_groups.listgroups_filename(groups)
+ if None in [g["group_name"] for g in groups]: # tous les etudiants
+ getallstudents = True
+ gr_title_filename = "tous"
+ else:
+ getallstudents = False
+ etudids = [
+ x[0]
+ for x in sco_groups.do_evaluation_listeetuds_groups(
+ evaluation_id, groups, getallstudents=getallstudents, include_demdef=True
+ )
+ ]
+
+ # une liste de liste de chaines: lignes de la feuille de calcul
+ rows = []
+ etuds = sco_saisie_notes.get_sorted_etuds_notes(
+ evaluation, etudids, formsemestre.id
+ )
+ for e in etuds:
+ etudid = e["etudid"]
+ groups = sco_groups.get_etud_groups(etudid, formsemestre.id)
+ grc = sco_groups.listgroups_abbrev(groups)
+ rows.append(
+ {
+ "etudid": str(etudid),
+ "code_nip": e["code_nip"],
+ "explanation": e["explanation"],
+ "nom": e.get("nom_disp", "") or e.get("nom_usuel", "") or e["nom"],
+ "prenom": e["prenom"].lower().capitalize(),
+ "etat": e["inscr"]["etat"],
+ "groupes": grc,
+ "note": e["val"],
+ }
+ )
+
+ filename = f"notes_{eval_name}_{gr_title_filename}"
+ xls = excel_feuille_saisie(
+ evaluation, formsemestre.titre_annee(), description, rows=rows
+ )
+ return scu.send_file(xls, filename, scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE)
+
+
+def do_evaluation_upload_xls():
+ """
+ Soumission d'un fichier XLS (evaluation_id, notefile)
+ """
+ args = scu.get_request_args()
+ evaluation_id = int(args["evaluation_id"])
+ comment = args["comment"]
+ evaluation = Evaluation.get_evaluation(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, lines = sco_excel.excel_file_to_list(args["notefile"])
+ try:
+ if not lines:
+ raise InvalidNoteValue()
+ # -- search eval code
+ n = len(lines)
+ i = 0
+ while i < n:
+ if not lines[i]:
+ diag.append("Erreur: format invalide (ligne vide ?)")
+ raise InvalidNoteValue()
+ f0 = lines[i][0].strip()
+ if f0 and f0[0] == "!":
+ break
+ i = i + 1
+ if i == n:
+ diag.append("Erreur: format invalide ! (pas de ligne evaluation_id)")
+ raise InvalidNoteValue()
+
+ eval_id_str = lines[i][0].strip()[1:]
+ try:
+ eval_id = int(eval_id_str)
+ except ValueError:
+ eval_id = None
+ if eval_id != evaluation_id:
+ diag.append(
+ f"""Erreur: fichier invalide: le code d'évaluation de correspond pas ! ('{
+ eval_id_str}' != '{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 Exception as exc:
+ diag.append(
+ f"""Erreur: Ligne invalide ! (erreur ligne {ni})
{lines[ni]}"""
+ )
+ raise InvalidNoteValue() from exc
+ # -- check values
+ valid_notes, invalids, withoutnotes, absents, _ = sco_saisie_notes.check_notes(
+ notes, evaluation
+ )
+ if invalids:
+ diag.append(
+ f"Erreur: la feuille contient {len(invalids)} notes invalides
+ {len(etudids_changed)} notes changées ({len(withoutnotes)} sans notes, + {len(absents)} absents, {nb_suppress} note supprimées) +
""" + if messages: + msg += f"""Important: il y avait déjà des décisions de jury + enregistrées, qui sont peut-être à revoir suite à cette modification !
+ """ + return 1, msg + + except InvalidNoteValue: + if diag: + msg = ( + '(pas de notes modifiées)
" + + +def saisie_notes_tableur(evaluation_id, group_ids=()): + """Saisie des notes via un fichier Excel""" + evaluation = Evaluation.query.get_or_404(evaluation_id) + moduleimpl_id = evaluation.moduleimpl.id + formsemestre_id = evaluation.moduleimpl.formsemestre_id + if not evaluation.moduleimpl.can_edit_notes(current_user): + return ( + html_sco_header.sco_header() + + f""" +(vérifiez que le semestre n'est pas verrouillé et que vous + avez l'autorisation d'effectuer cette opération) +
+ + """ + + html_sco_header.sco_footer() + ) + + page_title = "Saisie des notes" + ( + f""" de {evaluation.description}""" if evaluation.description else "" + ) + + # Informations sur les groupes à afficher: + groups_infos = sco_groups_view.DisplayedGroupsInfos( + group_ids=group_ids, + formsemestre_id=formsemestre_id, + select_all_when_unspecified=True, + etat=None, + ) + + H = [ + html_sco_header.sco_header( + page_title=page_title, + javascripts=sco_groups_view.JAVASCRIPTS, + cssstyles=sco_groups_view.CSSSTYLES, + init_qtip=True, + ), + sco_evaluations.evaluation_describe(evaluation_id=evaluation_id), + """Saisie des notes par fichier""", + ] + + # Menu choix groupe: + H.append("""""") + H.append(sco_groups_view.form_groups_choice(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() + if updiag[0]: + H.append(updiag[1]) + H.append( + f"""Notes chargées. + + Revenir au tableau de bord du module + + Charger un autre fichier de notes + + Formulaire de saisie des notes +
""" + ) + else: + H.append( + f""" +Notes non chargées !
+ {updiag[1]} + + """ + ) + + # + H.append("""- {len(etudids_changed)} notes changées ({len(withoutnotes)} sans notes, - {len(absents)} absents, {nb_suppress} note supprimées) -
""" - if messages: - msg += f"""Important: il y avait déjà des décisions de jury - enregistrées, qui sont peut-être à revoir suite à cette modification !
- """ - return 1, msg - - except InvalidNoteValue: - if diag: - msg = ( - '(pas de notes modifiées)
" - - 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) @@ -331,10 +177,10 @@ def do_evaluation_set_etud_note(evaluation: Evaluation, etud: Identite, value) - if not evaluation.moduleimpl.can_edit_notes(current_user): raise AccessDenied(f"Modification des notes impossible pour {current_user}") # Convert and check value - L, invalids, _, _, _ = _check_notes([(etud.id, value)], evaluation) + notes, invalids, _, _, _ = check_notes([(etud.id, value)], evaluation) if len(invalids) == 0: etudids_changed, _, _, _ = notes_add( - current_user, evaluation.id, L, "Initialisation notes" + current_user, evaluation.id, notes, "Initialisation notes" ) if len(etudids_changed) == 1: return True @@ -371,7 +217,7 @@ def do_evaluation_set_missing( if etudid not in notes_db: # pas de note notes.append((etudid, value)) # Convert and check values - valid_notes, invalids, _, _, _ = _check_notes(notes, evaluation) + valid_notes, invalids, _, _, _ = check_notes(notes, evaluation) dest_url = url_for( "notes.saisie_notes", scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id ) @@ -615,7 +461,7 @@ def notes_add( etudids_changed.append(etudid) if res.etud_has_decision(etudid, include_rcues=False): etudids_with_decision.append(etudid) - except Exception as exc: + except NotImplementedError as exc: # XXX log("*** exception in notes_add") if do_it: cnx.rollback() # abort @@ -675,12 +521,13 @@ def _record_note( else: # il y a deja une note oldval = notes_db[etudid]["value"] - if type(value) != type(oldval): - changed = True - elif isinstance(value, float) and (abs(value - oldval) > scu.NOTES_PRECISION): - changed = True - elif value != oldval: - changed = True + 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: @@ -731,284 +578,7 @@ def _record_note( return changed, suppressed -def saisie_notes_tableur(evaluation_id, group_ids=()): - """Saisie des notes via un fichier Excel""" - evaluation = Evaluation.query.get_or_404(evaluation_id) - moduleimpl_id = evaluation.moduleimpl.id - formsemestre_id = evaluation.moduleimpl.formsemestre_id - if not evaluation.moduleimpl.can_edit_notes(current_user): - return ( - html_sco_header.sco_header() - + f""" -(vérifiez que le semestre n'est pas verrouillé et que vous - avez l'autorisation d'effectuer cette opération) -
- - """ - + html_sco_header.sco_footer() - ) - - page_title = "Saisie des notes" + ( - f""" de {evaluation.description}""" if evaluation.description else "" - ) - - # Informations sur les groupes à afficher: - groups_infos = sco_groups_view.DisplayedGroupsInfos( - group_ids=group_ids, - formsemestre_id=formsemestre_id, - select_all_when_unspecified=True, - etat=None, - ) - - H = [ - html_sco_header.sco_header( - page_title=page_title, - javascripts=sco_groups_view.JAVASCRIPTS, - cssstyles=sco_groups_view.CSSSTYLES, - init_qtip=True, - ), - sco_evaluations.evaluation_describe(evaluation_id=evaluation_id), - """Saisie des notes par fichier""", - ] - - # Menu choix groupe: - H.append("""""") - H.append(sco_groups_view.form_groups_choice(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() - if updiag[0]: - H.append(updiag[1]) - H.append( - f"""Notes chargées. - - Revenir au tableau de bord du module - - Charger un autre fichier de notes - - Formulaire de saisie des notes -
""" - ) - else: - H.append( - f""" -Notes non chargées !
- {updiag[1]} - - """ - ) - - # - H.append("""