forked from ScoDoc/ScoDoc
Saisie note excel: améliore feuille et reorganise le code. + affichage date eval sans heures
This commit is contained in:
parent
69af0b9778
commit
9a289d5956
@ -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)}"
|
||||
)
|
||||
|
@ -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)
|
||||
|
655
app/scodoc/sco_saisie_excel.py
Normal file
655
app/scodoc/sco_saisie_excel.py
Normal file
@ -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})<br>{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</p>"
|
||||
)
|
||||
if len(invalids) < 25:
|
||||
etudsnames = [
|
||||
Identite.get_etud(etudid).nom_prenom() for etudid in invalids
|
||||
]
|
||||
diag.append("Notes invalides pour: " + ", ".join(etudsnames))
|
||||
raise InvalidNoteValue()
|
||||
|
||||
etudids_changed, nb_suppress, etudids_with_decisions, messages = (
|
||||
sco_saisie_notes.notes_add(
|
||||
current_user, evaluation_id, valid_notes, comment
|
||||
)
|
||||
)
|
||||
# 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,
|
||||
)
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_NOTE,
|
||||
obj=evaluation.moduleimpl_id,
|
||||
text=f"""Chargement notes dans <a href="{status_url}">{
|
||||
module.titre or module.code}</a>""",
|
||||
url=status_url,
|
||||
max_frequency=30 * 60, # 30 minutes
|
||||
)
|
||||
|
||||
msg = f"""<p>
|
||||
{len(etudids_changed)} notes changées ({len(withoutnotes)} sans notes,
|
||||
{len(absents)} absents, {nb_suppress} note supprimées)
|
||||
</p>"""
|
||||
if messages:
|
||||
msg += f"""<div class="warning">Attention :
|
||||
<ul>
|
||||
<li>{
|
||||
'</li><li>'.join(messages)
|
||||
}
|
||||
</li>
|
||||
</ul>
|
||||
</div>"""
|
||||
if etudids_with_decisions:
|
||||
msg += """<p class="warning">Important: il y avait déjà des décisions de jury
|
||||
enregistrées, qui sont peut-être à revoir suite à cette modification !</p>
|
||||
"""
|
||||
return 1, msg
|
||||
|
||||
except InvalidNoteValue:
|
||||
if diag:
|
||||
msg = (
|
||||
'<ul class="tf-msg"><li class="tf_msg">'
|
||||
+ '</li><li class="tf_msg">'.join(diag)
|
||||
+ "</li></ul>"
|
||||
)
|
||||
else:
|
||||
msg = '<ul class="tf-msg"><li class="tf_msg">Une erreur est survenue</li></ul>'
|
||||
return 0, msg + "<p>(pas de notes modifiées)</p>"
|
||||
|
||||
|
||||
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"""
|
||||
<h2>Modification des notes impossible pour {current_user.user_name}</h2>
|
||||
<p>(vérifiez que le semestre n'est pas verrouillé et que vous
|
||||
avez l'autorisation d'effectuer cette opération)
|
||||
</p>
|
||||
<p><a class="stdlink" href="{
|
||||
url_for("notes.moduleimpl_status", scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=moduleimpl_id)
|
||||
}">Continuer</a></p>
|
||||
"""
|
||||
+ 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),
|
||||
"""<span class="eval_title">Saisie des notes par fichier</span>""",
|
||||
]
|
||||
|
||||
# Menu choix groupe:
|
||||
H.append("""<div id="group-tabs"><table><tr><td>""")
|
||||
H.append(sco_groups_view.form_groups_choice(groups_infos))
|
||||
H.append("</td></tr></table></div>")
|
||||
|
||||
H.append(
|
||||
f"""<div class="saisienote_etape1">
|
||||
<span class="titredivsaisienote">Etape 1 : </span>
|
||||
<ul>
|
||||
<li><a class="stdlink" href="feuille_saisie_notes?evaluation_id={evaluation_id}&{
|
||||
groups_infos.groups_query_args}"
|
||||
id="lnk_feuille_saisie">obtenir le fichier tableur à remplir</a>
|
||||
</li>
|
||||
<li>ou <a class="stdlink" href="{url_for("notes.saisie_notes",
|
||||
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id)
|
||||
}">aller au formulaire de saisie</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<form>
|
||||
<input type="hidden" name="evaluation_id" id="formnotes_evaluation_id"
|
||||
value="{evaluation_id}"/>
|
||||
</form>
|
||||
"""
|
||||
)
|
||||
|
||||
H.append(
|
||||
"""<div class="saisienote_etape2">
|
||||
<span class="titredivsaisienote">Etape 2 : chargement d'un fichier de notes</span>""" # '
|
||||
)
|
||||
|
||||
nf = TrivialFormulator(
|
||||
request.base_url,
|
||||
scu.get_request_args(),
|
||||
(
|
||||
("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(
|
||||
"""<p>Le fichier doit être un fichier tableur obtenu via
|
||||
l'étape 1 ci-dessus, puis complété et enregistré au format Excel.
|
||||
</p>"""
|
||||
)
|
||||
H.append(nf[1])
|
||||
elif nf[0] == -1:
|
||||
H.append("<p>Annulation</p>")
|
||||
elif nf[0] == 1:
|
||||
updiag = do_evaluation_upload_xls()
|
||||
if updiag[0]:
|
||||
H.append(updiag[1])
|
||||
H.append(
|
||||
f"""<p>Notes chargées.
|
||||
<a class="stdlink" href="{
|
||||
url_for("notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id)
|
||||
}">
|
||||
Revenir au tableau de bord du module</a>
|
||||
|
||||
<a class="stdlink" href="{url_for("notes.saisie_notes_tableur",
|
||||
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id)
|
||||
}">Charger un autre fichier de notes</a>
|
||||
|
||||
<a class="stdlink" href="{url_for("notes.saisie_notes",
|
||||
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id)
|
||||
}">Formulaire de saisie des notes</a>
|
||||
</p>"""
|
||||
)
|
||||
else:
|
||||
H.append(
|
||||
f"""
|
||||
<p class="redboldtext">Notes non chargées !</p>
|
||||
{updiag[1]}
|
||||
<p><a class="stdlink" href="{url_for("notes.saisie_notes_tableur",
|
||||
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id)
|
||||
}">
|
||||
Reprendre</a>
|
||||
</p>
|
||||
"""
|
||||
)
|
||||
|
||||
#
|
||||
H.append("""</div><h3>Autres opérations</h3><ul>""")
|
||||
if evaluation.moduleimpl.can_edit_notes(current_user, allow_ens=False):
|
||||
H.append(
|
||||
f"""
|
||||
<li>
|
||||
<form action="do_evaluation_set_missing" method="POST">
|
||||
Mettre toutes les notes manquantes à <input type="text" size="5" name="value"/>
|
||||
<input type="submit" value="OK"/>
|
||||
<input type="hidden" name="evaluation_id" value="{evaluation_id}"/>
|
||||
<em>ABS indique "absent" (zéro), EXC "excusé" (neutralisées), ATT "attente"</em>
|
||||
</form>
|
||||
</li>
|
||||
<li><a class="stdlink" href="{url_for("notes.evaluation_suppress_alln",
|
||||
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id)
|
||||
}">Effacer toutes les notes de cette évaluation</a>
|
||||
(ceci permet ensuite de supprimer l'évaluation si besoin)
|
||||
</li>
|
||||
<li><a class="stdlink" href="{url_for("notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id)
|
||||
}">Revenir au module</a>
|
||||
</li>
|
||||
<li><a class="stdlink" href="{url_for("notes.saisie_notes",
|
||||
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id)
|
||||
}">Revenir au formulaire de saisie</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3>Explications</h3>
|
||||
<ol>
|
||||
<li>Etape 1:
|
||||
<ol><li>choisir le ou les groupes d'étudiants;</li>
|
||||
<li>télécharger le fichier Excel à remplir.</li>
|
||||
</ol>
|
||||
</li>
|
||||
<li>Etape 2 (cadre vert): Indiquer le fichier Excel
|
||||
<em>téléchargé à l'étape 1</em> et dans lequel on a saisi des notes.
|
||||
Remarques:
|
||||
<ul>
|
||||
<li>le fichier Excel peut être incomplet: on peut ne saisir que quelques notes
|
||||
et répéter l'opération (en téléchargeant un nouveau fichier) plus tard;
|
||||
</li>
|
||||
<li>seules les valeurs des notes modifiées sont prises en compte;
|
||||
</li>
|
||||
<li>seules les notes sont extraites du fichier Excel;
|
||||
</li>
|
||||
<li>on peut optionnellement ajouter un commentaire (type "copies corrigées
|
||||
par Dupont", ou "Modif. suite à contestation") dans la case "Commentaire".
|
||||
</li>
|
||||
<li>le fichier Excel <em>doit impérativement être celui chargé à l'étape 1
|
||||
pour cette évaluation</em>. Il n'est pas possible d'utiliser une liste d'appel
|
||||
ou autre document Excel téléchargé d'une autre page.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
"""
|
||||
)
|
||||
H.append(html_sco_header.sco_footer())
|
||||
return "\n".join(H)
|
@ -1,6 +1,3 @@
|
||||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
@ -34,7 +31,7 @@ import time
|
||||
|
||||
|
||||
import flask
|
||||
from flask import g, url_for, request
|
||||
from flask import g, url_for
|
||||
from flask_login import current_user
|
||||
from flask_sqlalchemy.query import Query
|
||||
import psycopg2
|
||||
@ -55,25 +52,23 @@ from app.models.etudiants import Identite
|
||||
|
||||
from app.scodoc.sco_exceptions import (
|
||||
AccessDenied,
|
||||
InvalidNoteValue,
|
||||
NoteProcessError,
|
||||
ScoException,
|
||||
ScoInvalidParamError,
|
||||
ScoValueError,
|
||||
)
|
||||
from app.scodoc import html_sco_header, sco_users
|
||||
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_excel
|
||||
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 TrivialFormulator, TF
|
||||
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
|
||||
@ -116,25 +111,7 @@ def convert_note_from_string(
|
||||
return note_value, invalid
|
||||
|
||||
|
||||
def _display_note(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 == scu.NOTES_NEUTRALISE:
|
||||
val = "EXC" # excuse, note neutralise
|
||||
elif val == scu.NOTES_ATTENTE:
|
||||
val = "ATT" # attente, note neutralise
|
||||
elif val == scu.NOTES_SUPPRESS:
|
||||
val = "SUPR"
|
||||
else:
|
||||
val = "%g" % val
|
||||
return val
|
||||
|
||||
|
||||
def _check_notes(notes: list[(int, float | str)], evaluation: Evaluation):
|
||||
def check_notes(notes: list[(int, float | str)], evaluation: Evaluation):
|
||||
"""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)
|
||||
@ -193,137 +170,6 @@ def _check_notes(notes: list[(int, float | str)], evaluation: Evaluation):
|
||||
)
|
||||
|
||||
|
||||
def do_evaluation_upload_xls():
|
||||
"""
|
||||
Soumission d'un fichier XLS (evaluation_id, notefile)
|
||||
"""
|
||||
vals = scu.get_request_args()
|
||||
evaluation_id = int(vals["evaluation_id"])
|
||||
comment = vals["comment"]
|
||||
evaluation: Evaluation = Evaluation.query.get_or_404(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(vals["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})<br>{lines[ni]}"""
|
||||
)
|
||||
raise InvalidNoteValue() from exc
|
||||
# -- check values
|
||||
valid_notes, invalids, withoutnotes, absents, _ = _check_notes(
|
||||
notes, evaluation
|
||||
)
|
||||
if invalids:
|
||||
diag.append(
|
||||
f"Erreur: la feuille contient {len(invalids)} notes invalides</p>"
|
||||
)
|
||||
if len(invalids) < 25:
|
||||
etudsnames = [
|
||||
sco_etud.get_etud_info(etudid=etudid, filled=True)[0]["nomprenom"]
|
||||
for etudid in invalids
|
||||
]
|
||||
diag.append("Notes invalides pour: " + ", ".join(etudsnames))
|
||||
raise InvalidNoteValue()
|
||||
else:
|
||||
etudids_changed, nb_suppress, etudids_with_decisions, messages = notes_add(
|
||||
current_user, evaluation_id, valid_notes, comment
|
||||
)
|
||||
# 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,
|
||||
)
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_NOTE,
|
||||
obj=evaluation.moduleimpl_id,
|
||||
text=f"""Chargement notes dans <a href="{status_url}">{
|
||||
module.titre or module.code}</a>""",
|
||||
url=status_url,
|
||||
max_frequency=30 * 60, # 30 minutes
|
||||
)
|
||||
|
||||
msg = f"""<p>
|
||||
{len(etudids_changed)} notes changées ({len(withoutnotes)} sans notes,
|
||||
{len(absents)} absents, {nb_suppress} note supprimées)
|
||||
</p>"""
|
||||
if messages:
|
||||
msg += f"""<div class="warning">Attention :
|
||||
<ul>
|
||||
<li>{
|
||||
'</li><li>'.join(messages)
|
||||
}
|
||||
</li>
|
||||
</ul>
|
||||
</div>"""
|
||||
if etudids_with_decisions:
|
||||
msg += """<p class="warning">Important: il y avait déjà des décisions de jury
|
||||
enregistrées, qui sont peut-être à revoir suite à cette modification !</p>
|
||||
"""
|
||||
return 1, msg
|
||||
|
||||
except InvalidNoteValue:
|
||||
if diag:
|
||||
msg = (
|
||||
'<ul class="tf-msg"><li class="tf_msg">'
|
||||
+ '</li><li class="tf_msg">'.join(diag)
|
||||
+ "</li></ul>"
|
||||
)
|
||||
else:
|
||||
msg = '<ul class="tf-msg"><li class="tf_msg">Une erreur est survenue</li></ul>'
|
||||
return 0, msg + "<p>(pas de notes modifiées)</p>"
|
||||
|
||||
|
||||
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"""
|
||||
<h2>Modification des notes impossible pour {current_user.user_name}</h2>
|
||||
<p>(vérifiez que le semestre n'est pas verrouillé et que vous
|
||||
avez l'autorisation d'effectuer cette opération)
|
||||
</p>
|
||||
<p><a class="stdlink" href="{
|
||||
url_for("notes.moduleimpl_status", scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=moduleimpl_id)
|
||||
}">Continuer</a></p>
|
||||
"""
|
||||
+ 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),
|
||||
"""<span class="eval_title">Saisie des notes par fichier</span>""",
|
||||
]
|
||||
|
||||
# Menu choix groupe:
|
||||
H.append("""<div id="group-tabs"><table><tr><td>""")
|
||||
H.append(sco_groups_view.form_groups_choice(groups_infos))
|
||||
H.append("</td></tr></table></div>")
|
||||
|
||||
H.append(
|
||||
f"""<div class="saisienote_etape1">
|
||||
<span class="titredivsaisienote">Etape 1 : </span>
|
||||
<ul>
|
||||
<li><a class="stdlink" href="feuille_saisie_notes?evaluation_id={evaluation_id}&{
|
||||
groups_infos.groups_query_args}"
|
||||
id="lnk_feuille_saisie">obtenir le fichier tableur à remplir</a>
|
||||
</li>
|
||||
<li>ou <a class="stdlink" href="{url_for("notes.saisie_notes",
|
||||
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id)
|
||||
}">aller au formulaire de saisie</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<form>
|
||||
<input type="hidden" name="evaluation_id" id="formnotes_evaluation_id"
|
||||
value="{evaluation_id}"/>
|
||||
</form>
|
||||
"""
|
||||
)
|
||||
|
||||
H.append(
|
||||
"""<div class="saisienote_etape2">
|
||||
<span class="titredivsaisienote">Etape 2 : chargement d'un fichier de notes</span>""" # '
|
||||
)
|
||||
|
||||
nf = TrivialFormulator(
|
||||
request.base_url,
|
||||
scu.get_request_args(),
|
||||
(
|
||||
("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(
|
||||
"""<p>Le fichier doit être un fichier tableur obtenu via
|
||||
l'étape 1 ci-dessus, puis complété et enregistré au format Excel.
|
||||
</p>"""
|
||||
)
|
||||
H.append(nf[1])
|
||||
elif nf[0] == -1:
|
||||
H.append("<p>Annulation</p>")
|
||||
elif nf[0] == 1:
|
||||
updiag = do_evaluation_upload_xls()
|
||||
if updiag[0]:
|
||||
H.append(updiag[1])
|
||||
H.append(
|
||||
f"""<p>Notes chargées.
|
||||
<a class="stdlink" href="{
|
||||
url_for("notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id)
|
||||
}">
|
||||
Revenir au tableau de bord du module</a>
|
||||
|
||||
<a class="stdlink" href="{url_for("notes.saisie_notes_tableur",
|
||||
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id)
|
||||
}">Charger un autre fichier de notes</a>
|
||||
|
||||
<a class="stdlink" href="{url_for("notes.saisie_notes",
|
||||
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id)
|
||||
}">Formulaire de saisie des notes</a>
|
||||
</p>"""
|
||||
)
|
||||
else:
|
||||
H.append(
|
||||
f"""
|
||||
<p class="redboldtext">Notes non chargées !</p>
|
||||
{updiag[1]}
|
||||
<p><a class="stdlink" href="{url_for("notes.saisie_notes_tableur",
|
||||
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id)
|
||||
}">
|
||||
Reprendre</a>
|
||||
</p>
|
||||
"""
|
||||
)
|
||||
|
||||
#
|
||||
H.append("""</div><h3>Autres opérations</h3><ul>""")
|
||||
if evaluation.moduleimpl.can_edit_notes(current_user, allow_ens=False):
|
||||
H.append(
|
||||
f"""
|
||||
<li>
|
||||
<form action="do_evaluation_set_missing" method="POST">
|
||||
Mettre toutes les notes manquantes à <input type="text" size="5" name="value"/>
|
||||
<input type="submit" value="OK"/>
|
||||
<input type="hidden" name="evaluation_id" value="{evaluation_id}"/>
|
||||
<em>ABS indique "absent" (zéro), EXC "excusé" (neutralisées), ATT "attente"</em>
|
||||
</form>
|
||||
</li>
|
||||
<li><a class="stdlink" href="{url_for("notes.evaluation_suppress_alln",
|
||||
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id)
|
||||
}">Effacer toutes les notes de cette évaluation</a>
|
||||
(ceci permet ensuite de supprimer l'évaluation si besoin)
|
||||
</li>
|
||||
<li><a class="stdlink" href="{url_for("notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id)
|
||||
}">Revenir au module</a>
|
||||
</li>
|
||||
<li><a class="stdlink" href="{url_for("notes.saisie_notes",
|
||||
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id)
|
||||
}">Revenir au formulaire de saisie</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3>Explications</h3>
|
||||
<ol>
|
||||
<li>Etape 1:
|
||||
<ol><li>choisir le ou les groupes d'étudiants;</li>
|
||||
<li>télécharger le fichier Excel à remplir.</li>
|
||||
</ol>
|
||||
</li>
|
||||
<li>Etape 2 (cadre vert): Indiquer le fichier Excel
|
||||
<em>téléchargé à l'étape 1</em> et dans lequel on a saisi des notes.
|
||||
Remarques:
|
||||
<ul>
|
||||
<li>le fichier Excel peut être incomplet: on peut ne saisir que quelques notes
|
||||
et répéter l'opération (en téléchargeant un nouveau fichier) plus tard;
|
||||
</li>
|
||||
<li>seules les valeurs des notes modifiées sont prises en compte;
|
||||
</li>
|
||||
<li>seules les notes sont extraites du fichier Excel;
|
||||
</li>
|
||||
<li>on peut optionnellement ajouter un commentaire (type "copies corrigées
|
||||
par Dupont", ou "Modif. suite à contestation") dans la case "Commentaire".
|
||||
</li>
|
||||
<li>le fichier Excel <em>doit impérativement être celui chargé à l'étape 1
|
||||
pour cette évaluation</em>. Il n'est pas possible d'utiliser une liste d'appel
|
||||
ou autre document Excel téléchargé d'une autre page.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
"""
|
||||
)
|
||||
H.append(html_sco_header.sco_footer())
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
def feuille_saisie_notes(evaluation_id, group_ids: list[int] = None):
|
||||
"""Document Excel pour saisie notes dans l'évaluation et les groupes indiqués"""
|
||||
evaluation: Evaluation = db.session.get(Evaluation, evaluation_id)
|
||||
if not evaluation:
|
||||
raise ScoValueError("invalid 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 = _get_sorted_etuds(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(
|
||||
[
|
||||
str(etudid),
|
||||
e.get("nom_disp", "") or e.get("nom_usuel", "") or e["nom"],
|
||||
e["prenom"].lower().capitalize(),
|
||||
e["inscr"]["etat"],
|
||||
grc,
|
||||
e["val"],
|
||||
e["explanation"],
|
||||
e["code_nip"],
|
||||
]
|
||||
)
|
||||
|
||||
filename = f"notes_{eval_name}_{gr_title_filename}"
|
||||
xls = sco_excel.excel_feuille_saisie(
|
||||
evaluation, formsemestre.titre_annee(), description, lines=rows
|
||||
)
|
||||
return scu.send_file(xls, filename, scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE)
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# 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):
|
||||
@ -1095,9 +665,11 @@ def saisie_notes(evaluation_id: int, group_ids: list = None):
|
||||
)
|
||||
H.append(
|
||||
"""
|
||||
</td>
|
||||
<td style="padding-left: 35px;"><button class="btn_masquer_DEM">Masquer les DEM</button></td>
|
||||
</tr>
|
||||
</td>
|
||||
<td style="padding-left: 35px;">
|
||||
<button class="btn_masquer_DEM">Masquer les DEM</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<style>
|
||||
@ -1144,8 +716,10 @@ def saisie_notes(evaluation_id: int, group_ids: list = None):
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
def _get_sorted_etuds(evaluation: Evaluation, etudids: list, formsemestre_id: int):
|
||||
# Notes existantes
|
||||
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 = []
|
||||
@ -1182,7 +756,9 @@ def _get_sorted_etuds(evaluation: Evaluation, etudids: list, formsemestre_id: in
|
||||
|
||||
# Note actuelle de l'étudiant:
|
||||
if etudid in notes_db:
|
||||
e["val"] = _display_note(notes_db[etudid]["value"])
|
||||
e["val"] = scu.fmt_note(
|
||||
notes_db[etudid]["value"], fixed_precision_str=False
|
||||
)
|
||||
comment = notes_db[etudid]["comment"]
|
||||
if comment is None:
|
||||
comment = ""
|
||||
@ -1236,7 +812,7 @@ def _form_saisie_notes(
|
||||
# Nb de décisions de jury (pour les inscrits à l'évaluation):
|
||||
nb_decisions = sum(decisions_jury.values())
|
||||
|
||||
etuds = _get_sorted_etuds(evaluation, etudids, formsemestre_id)
|
||||
etuds = get_sorted_etuds_notes(evaluation, etudids, formsemestre_id)
|
||||
|
||||
# Build form:
|
||||
descr = [
|
||||
@ -1263,7 +839,7 @@ def _form_saisie_notes(
|
||||
"title": "Notes ",
|
||||
"cssclass": "formnote_bareme",
|
||||
"readonly": True,
|
||||
"default": " / %g" % evaluation.note_max,
|
||||
"default": f" / {evaluation.note_max:g}",
|
||||
},
|
||||
)
|
||||
)
|
||||
@ -1299,13 +875,8 @@ def _form_saisie_notes(
|
||||
for group_info in e["groups"]:
|
||||
etud_classes.append("group-" + str(group_info["group_id"]))
|
||||
|
||||
label = (
|
||||
'<span class="%s">' % classdem
|
||||
+ e["civilite_str"]
|
||||
+ " "
|
||||
+ scu.format_nomprenom(e, reverse=True)
|
||||
+ "</span>"
|
||||
)
|
||||
label = f"""<span class="{classdem}">{e["civilite_str"]} {
|
||||
scu.format_nomprenom(e, reverse=True)}</span>"""
|
||||
|
||||
# Historique des saisies de notes:
|
||||
explanation = (
|
||||
@ -1322,7 +893,7 @@ def _form_saisie_notes(
|
||||
|
||||
# Valeur actuelle du champ:
|
||||
initvalues["note_" + str(etudid)] = e["val"]
|
||||
label_link = '<a class="etudinfo" id="%s">%s</a>' % (etudid, label)
|
||||
label_link = f'<a class="etudinfo" id="{etudid}">{label}</a>'
|
||||
|
||||
# Element de formulaire:
|
||||
descr.append(
|
||||
@ -1334,11 +905,11 @@ def _form_saisie_notes(
|
||||
"explanation": explanation,
|
||||
"return_focus_next": True,
|
||||
"attributes": [
|
||||
'class="note%s"' % classdem,
|
||||
f'class="note{classdem}"',
|
||||
disabled_attr,
|
||||
'data-last-saved-value="%s"' % e["val"],
|
||||
'data-orig-value="%s"' % e["val"],
|
||||
'data-etudid="%s"' % etudid,
|
||||
f'''data-last-saved-value="{e['val']}"''',
|
||||
f'''data-orig-value="{e["val"]}"''',
|
||||
f'data-etudid="{etudid}"',
|
||||
],
|
||||
"template": """<tr%(item_dom_attr)s class="etud_elem """
|
||||
+ " ".join(etud_classes)
|
||||
@ -1422,7 +993,7 @@ def save_notes(
|
||||
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)
|
||||
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
|
||||
@ -1464,8 +1035,7 @@ def get_note_history_menu(evaluation_id: int, etudid: int) -> str:
|
||||
H = []
|
||||
if len(history) > 1:
|
||||
H.append(
|
||||
'<select data-etudid="%s" class="note_history" onchange="change_history(this);">'
|
||||
% etudid
|
||||
f'<select data-etudid="{etudid}" class="note_history" onchange="change_history(this);">'
|
||||
)
|
||||
envir = "select"
|
||||
item = "option"
|
||||
@ -1477,21 +1047,19 @@ def get_note_history_menu(evaluation_id: int, etudid: int) -> str:
|
||||
|
||||
first = True
|
||||
for i in history:
|
||||
jt = i["date"].strftime("le %d/%m/%Y à %H:%M") + " (%s)" % i["user_name"]
|
||||
dispnote = _display_note(i["value"])
|
||||
jt = f"""{i["date"].strftime("le %d/%m/%Y à %H:%M")} ({i["user_name"]})"""
|
||||
dispnote = scu.fmt_note(i["value"], fixed_precision_str=False)
|
||||
if first:
|
||||
nv = "" # ne repete pas la valeur de la note courante
|
||||
else:
|
||||
# ancienne valeur
|
||||
nv = ": %s" % dispnote
|
||||
nv = f": {dispnote}"
|
||||
first = False
|
||||
if i["comment"]:
|
||||
comment = ' <span class="histcomment">%s</span>' % i["comment"]
|
||||
comment = f' <span class="histcomment">{i["comment"]}</span>'
|
||||
else:
|
||||
comment = ""
|
||||
H.append(
|
||||
'<%s data-note="%s">%s %s%s</%s>' % (item, dispnote, jt, nv, comment, item)
|
||||
)
|
||||
H.append(f'<{item} data-note="{dispnote}">{jt} {nv}{comment}</{item}>')
|
||||
|
||||
H.append("</%s>" % envir)
|
||||
H.append(f"</{envir}>")
|
||||
return "\n".join(H)
|
||||
|
@ -560,9 +560,13 @@ DATEATIME_FMT = DATE_FMT + " à " + TIME_FMT
|
||||
DATETIME_FMT = DATE_FMT + " " + TIME_FMT
|
||||
|
||||
|
||||
def fmt_note(val, note_max=None, keep_numeric=False):
|
||||
def fmt_note(
|
||||
val, note_max=None, keep_numeric=False, fixed_precision_str=True
|
||||
) -> str | float:
|
||||
"""conversion note en str pour affichage dans tables HTML ou PDF.
|
||||
Si keep_numeric, laisse les valeur numeriques telles quelles (pour export Excel)
|
||||
Si fixed_precision_str (défaut), formatte sur 4 chiffres ("01.23"),
|
||||
sinon utilise %g (chaine précision variable, pour les formulaires)
|
||||
"""
|
||||
if val is None or val == NOTES_ABSENCE:
|
||||
return "ABS"
|
||||
@ -570,6 +574,8 @@ def fmt_note(val, note_max=None, keep_numeric=False):
|
||||
return "EXC" # excuse, note neutralise
|
||||
if val == NOTES_ATTENTE:
|
||||
return "ATT" # attente, note neutralisee
|
||||
if val == NOTES_SUPPRESS:
|
||||
return "SUPR" # pour les formulaires de saisie
|
||||
if not isinstance(val, str):
|
||||
if np.isnan(val):
|
||||
return "~"
|
||||
@ -577,12 +583,13 @@ def fmt_note(val, note_max=None, keep_numeric=False):
|
||||
val = val * 20.0 / note_max
|
||||
if keep_numeric:
|
||||
return val
|
||||
else:
|
||||
s = "%2.2f" % round(float(val), 2) # 2 chiffres apres la virgule
|
||||
if fixed_precision_str:
|
||||
s = f"{round(float(val), 2):2.2f}" # 2 chiffres apres la virgule
|
||||
s = "0" * (5 - len(s)) + s # padding: 0 à gauche pour longueur 5: "12.34"
|
||||
return s
|
||||
else:
|
||||
return val.replace("NA", "-")
|
||||
return f"{val:g}"
|
||||
|
||||
return val.replace("NA", "-")
|
||||
|
||||
|
||||
def fmt_coef(val):
|
||||
|
@ -147,6 +147,7 @@ from app.scodoc import (
|
||||
sco_recapcomplet,
|
||||
sco_report,
|
||||
sco_report_but,
|
||||
sco_saisie_excel,
|
||||
sco_saisie_notes,
|
||||
sco_semset,
|
||||
sco_synchro_etuds,
|
||||
@ -1827,13 +1828,13 @@ sco_publish(
|
||||
# --- Saisie des notes
|
||||
sco_publish(
|
||||
"/saisie_notes_tableur",
|
||||
sco_saisie_notes.saisie_notes_tableur,
|
||||
sco_saisie_excel.saisie_notes_tableur,
|
||||
Permission.EnsView,
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
sco_publish(
|
||||
"/feuille_saisie_notes",
|
||||
sco_saisie_notes.feuille_saisie_notes,
|
||||
sco_saisie_excel.feuille_saisie_notes,
|
||||
Permission.EnsView,
|
||||
)
|
||||
sco_publish("/saisie_notes", sco_saisie_notes.saisie_notes, Permission.EnsView)
|
||||
|
@ -1,7 +1,7 @@
|
||||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
SCOVERSION = "9.6.984"
|
||||
SCOVERSION = "9.6.985"
|
||||
|
||||
SCONAME = "ScoDoc"
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user