Saisie note excel: améliore feuille et reorganise le code. + affichage date eval sans heures

This commit is contained in:
Emmanuel Viennet 2024-06-28 19:03:20 +02:00
parent 69af0b9778
commit 9a289d5956
7 changed files with 794 additions and 682 deletions

View File

@ -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)}"
)

View File

@ -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)

View 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&nbsp;:
<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.&nbsp;&nbsp;&nbsp;
<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>
&nbsp;&nbsp;&nbsp;
<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>
&nbsp;&nbsp;&nbsp;
<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)

View File

@ -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&nbsp;:
<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.&nbsp;&nbsp;&nbsp;
<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>
&nbsp;&nbsp;&nbsp;
<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>
&nbsp;&nbsp;&nbsp;
<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):
@ -1096,7 +666,9 @@ 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>
<td style="padding-left: 35px;">
<button class="btn_masquer_DEM">Masquer les DEM</button>
</td>
</tr>
</table>
</div>
@ -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": "&nbsp;/ %g" % evaluation.note_max,
"default": f"&nbsp;/ {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)

View File

@ -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,11 +583,12 @@ 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 f"{val:g}"
return val.replace("NA", "-")

View File

@ -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)

View File

@ -1,7 +1,7 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
SCOVERSION = "9.6.984"
SCOVERSION = "9.6.985"
SCONAME = "ScoDoc"