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)}"
|
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.date() == self.date_fin.date(): # même jour
|
||||||
if self.date_debut.time() == self.date_fin.time():
|
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 (
|
return (
|
||||||
f"le {self.date_debut.strftime('%d/%m/%Y')} à {_h(self.date_debut)}"
|
f"le {self.date_debut.strftime('%d/%m/%Y')} à {_h(self.date_debut)}"
|
||||||
)
|
)
|
||||||
|
@ -244,7 +244,10 @@ class ScoExcelSheet:
|
|||||||
return style
|
return style
|
||||||
|
|
||||||
@staticmethod
|
@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
|
if idx < 26: # one letter key
|
||||||
return chr(idx + 65)
|
return chr(idx + 65)
|
||||||
else: # two letters AA..ZZ
|
else: # two letters AA..ZZ
|
||||||
@ -252,19 +255,26 @@ class ScoExcelSheet:
|
|||||||
second = (idx % 26) + 65
|
second = (idx % 26) + 65
|
||||||
return "" + chr(first) + chr(second)
|
return "" + chr(first) + chr(second)
|
||||||
|
|
||||||
def set_column_dimension_width(self, cle=None, value=21):
|
def set_column_dimension_width(self, cle=None, value: int | str | list = 21):
|
||||||
"""Détermine la largeur d'une colonne. cle -- identifie la colonne ("A" "B", ... ou 0, 1, 2, ...) si None,
|
"""Détermine la largeur d'une colonne.
|
||||||
value donne la liste des largeurs de colonnes depuis A, B, C, ... value -- la dimension (unité : 7 pixels
|
cle -- identifie la colonne (lettre ou indice à partir de 0),
|
||||||
comme affiché dans Excel)
|
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:
|
if cle is None:
|
||||||
for i, val in enumerate(value):
|
cols_widths = 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
|
|
||||||
else:
|
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):
|
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,
|
"""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
|
self.ws.column_dimensions[cle].hidden = value
|
||||||
|
|
||||||
def set_auto_filter(self, range):
|
def set_auto_filter(self, filter_range):
|
||||||
self.auto_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.
|
"""Construit une cellule.
|
||||||
value -- contenu de la cellule (texte, numérique, booléen ou date)
|
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é
|
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:
|
elif value is False:
|
||||||
value = 0
|
value = 0
|
||||||
elif isinstance(value, datetime.datetime):
|
elif isinstance(value, datetime.datetime):
|
||||||
value = value.replace(
|
# make date naive (cf https://openpyxl.readthedocs.io/en/latest/datetime.html#timezones)
|
||||||
tzinfo=None
|
value = value.replace(tzinfo=None)
|
||||||
) # make date naive (cf https://openpyxl.readthedocs.io/en/latest/datetime.html#timezones)
|
|
||||||
|
|
||||||
# création de la cellule
|
# création de la cellule
|
||||||
cell = WriteOnlyCell(self.ws, value)
|
cell = WriteOnlyCell(self.ws, value)
|
||||||
@ -358,13 +405,14 @@ class ScoExcelSheet:
|
|||||||
for value, comment in zip(values, comments)
|
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.
|
"""construit une ligne composée d'une seule cellule et l'ajoute à la feuille.
|
||||||
mêmes paramètres que make_cell:
|
mêmes paramètres que make_cell:
|
||||||
value -- contenu de la cellule (texte ou numérique)
|
value -- contenu de la cellule (texte ou numérique)
|
||||||
style -- style par défaut de la feuille si non spécifié
|
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):
|
def append_blank_row(self):
|
||||||
"""construit une ligne vide et l'ajoute à la feuille."""
|
"""construit une ligne vide et l'ajoute à la feuille."""
|
||||||
@ -386,14 +434,21 @@ class ScoExcelSheet:
|
|||||||
for row in self.rows:
|
for row in self.rows:
|
||||||
self.ws.append(row)
|
self.ws.append(row)
|
||||||
|
|
||||||
def generate(self):
|
def generate(self, column_widths=None):
|
||||||
"""génération d'un classeur mono-feuille"""
|
"""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
|
if self.wb is None: # embeded sheet
|
||||||
raise ScoValueError("can't generate a single sheet from a ScoWorkbook")
|
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()
|
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:
|
if self.auto_filter is not None:
|
||||||
self.ws.auto_filter.ref = self.auto_filter
|
self.ws.auto_filter.ref = self.auto_filter
|
||||||
with NamedTemporaryFile() as tmp:
|
with NamedTemporaryFile() as tmp:
|
||||||
@ -446,182 +501,6 @@ def excel_simple_table(
|
|||||||
return ws.generate()
|
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):
|
def excel_bytes_to_list(bytes_content):
|
||||||
try:
|
try:
|
||||||
filelike = io.BytesIO(bytes_content)
|
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
|
# Gestion scolarite IUT
|
||||||
@ -34,7 +31,7 @@ import time
|
|||||||
|
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
from flask import g, url_for, request
|
from flask import g, url_for
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_sqlalchemy.query import Query
|
from flask_sqlalchemy.query import Query
|
||||||
import psycopg2
|
import psycopg2
|
||||||
@ -55,25 +52,23 @@ from app.models.etudiants import Identite
|
|||||||
|
|
||||||
from app.scodoc.sco_exceptions import (
|
from app.scodoc.sco_exceptions import (
|
||||||
AccessDenied,
|
AccessDenied,
|
||||||
InvalidNoteValue,
|
|
||||||
NoteProcessError,
|
NoteProcessError,
|
||||||
ScoException,
|
ScoException,
|
||||||
ScoInvalidParamError,
|
ScoInvalidParamError,
|
||||||
ScoValueError,
|
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 htmlutils
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
from app.scodoc import sco_etud
|
from app.scodoc import sco_etud
|
||||||
from app.scodoc import sco_evaluation_db
|
from app.scodoc import sco_evaluation_db
|
||||||
from app.scodoc import sco_evaluations
|
from app.scodoc import sco_evaluations
|
||||||
from app.scodoc import sco_excel
|
|
||||||
from app.scodoc import sco_formsemestre_inscriptions
|
from app.scodoc import sco_formsemestre_inscriptions
|
||||||
from app.scodoc import sco_groups
|
from app.scodoc import sco_groups
|
||||||
from app.scodoc import sco_groups_view
|
from app.scodoc import sco_groups_view
|
||||||
from app.scodoc import sco_undo_notes
|
from app.scodoc import sco_undo_notes
|
||||||
import app.scodoc.notesdb as ndb
|
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
|
import app.scodoc.sco_utils as scu
|
||||||
from app.scodoc.sco_utils import json_error
|
from app.scodoc.sco_utils import json_error
|
||||||
from app.scodoc.sco_utils import ModuleType
|
from app.scodoc.sco_utils import ModuleType
|
||||||
@ -116,25 +111,7 @@ def convert_note_from_string(
|
|||||||
return note_value, invalid
|
return note_value, invalid
|
||||||
|
|
||||||
|
|
||||||
def _display_note(val):
|
def check_notes(notes: list[(int, float | str)], evaluation: Evaluation):
|
||||||
"""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):
|
|
||||||
"""notes is a list of tuples (etudid, value)
|
"""notes is a list of tuples (etudid, value)
|
||||||
mod is the module (used to ckeck type, for malus)
|
mod is the module (used to ckeck type, for malus)
|
||||||
returns list of valid notes (etudid, float value)
|
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:
|
def do_evaluation_set_etud_note(evaluation: Evaluation, etud: Identite, value) -> bool:
|
||||||
"""Enregistre la note d'un seul étudiant
|
"""Enregistre la note d'un seul étudiant
|
||||||
value: valeur externe (float ou str)
|
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):
|
if not evaluation.moduleimpl.can_edit_notes(current_user):
|
||||||
raise AccessDenied(f"Modification des notes impossible pour {current_user}")
|
raise AccessDenied(f"Modification des notes impossible pour {current_user}")
|
||||||
# Convert and check value
|
# Convert and check value
|
||||||
L, invalids, _, _, _ = _check_notes([(etud.id, value)], evaluation)
|
notes, invalids, _, _, _ = check_notes([(etud.id, value)], evaluation)
|
||||||
if len(invalids) == 0:
|
if len(invalids) == 0:
|
||||||
etudids_changed, _, _, _ = notes_add(
|
etudids_changed, _, _, _ = notes_add(
|
||||||
current_user, evaluation.id, L, "Initialisation notes"
|
current_user, evaluation.id, notes, "Initialisation notes"
|
||||||
)
|
)
|
||||||
if len(etudids_changed) == 1:
|
if len(etudids_changed) == 1:
|
||||||
return True
|
return True
|
||||||
@ -371,7 +217,7 @@ def do_evaluation_set_missing(
|
|||||||
if etudid not in notes_db: # pas de note
|
if etudid not in notes_db: # pas de note
|
||||||
notes.append((etudid, value))
|
notes.append((etudid, value))
|
||||||
# Convert and check values
|
# Convert and check values
|
||||||
valid_notes, invalids, _, _, _ = _check_notes(notes, evaluation)
|
valid_notes, invalids, _, _, _ = check_notes(notes, evaluation)
|
||||||
dest_url = url_for(
|
dest_url = url_for(
|
||||||
"notes.saisie_notes", scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id
|
"notes.saisie_notes", scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id
|
||||||
)
|
)
|
||||||
@ -615,7 +461,7 @@ def notes_add(
|
|||||||
etudids_changed.append(etudid)
|
etudids_changed.append(etudid)
|
||||||
if res.etud_has_decision(etudid, include_rcues=False):
|
if res.etud_has_decision(etudid, include_rcues=False):
|
||||||
etudids_with_decision.append(etudid)
|
etudids_with_decision.append(etudid)
|
||||||
except Exception as exc:
|
except NotImplementedError as exc: # XXX
|
||||||
log("*** exception in notes_add")
|
log("*** exception in notes_add")
|
||||||
if do_it:
|
if do_it:
|
||||||
cnx.rollback() # abort
|
cnx.rollback() # abort
|
||||||
@ -675,12 +521,13 @@ def _record_note(
|
|||||||
else:
|
else:
|
||||||
# il y a deja une note
|
# il y a deja une note
|
||||||
oldval = notes_db[etudid]["value"]
|
oldval = notes_db[etudid]["value"]
|
||||||
if type(value) != type(oldval):
|
changed = (
|
||||||
changed = True
|
(not isinstance(value, type(oldval)))
|
||||||
elif isinstance(value, float) and (abs(value - oldval) > scu.NOTES_PRECISION):
|
or (
|
||||||
changed = True
|
isinstance(value, float) and (abs(value - oldval) > scu.NOTES_PRECISION)
|
||||||
elif value != oldval:
|
)
|
||||||
changed = True
|
or value != oldval
|
||||||
|
)
|
||||||
if changed:
|
if changed:
|
||||||
# recopie l'ancienne note dans notes_notes_log, puis update
|
# recopie l'ancienne note dans notes_notes_log, puis update
|
||||||
if do_it:
|
if do_it:
|
||||||
@ -731,284 +578,7 @@ def _record_note(
|
|||||||
return changed, suppressed
|
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)
|
# Nouveau formulaire saisie notes (2016)
|
||||||
|
|
||||||
|
|
||||||
def saisie_notes(evaluation_id: int, group_ids: list = None):
|
def saisie_notes(evaluation_id: int, group_ids: list = None):
|
||||||
"""Formulaire saisie notes d'une évaluation pour un groupe"""
|
"""Formulaire saisie notes d'une évaluation pour un groupe"""
|
||||||
if not isinstance(evaluation_id, int):
|
if not isinstance(evaluation_id, int):
|
||||||
@ -1096,7 +666,9 @@ def saisie_notes(evaluation_id: int, group_ids: list = None):
|
|||||||
H.append(
|
H.append(
|
||||||
"""
|
"""
|
||||||
</td>
|
</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>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@ -1144,8 +716,10 @@ def saisie_notes(evaluation_id: int, group_ids: list = None):
|
|||||||
return "\n".join(H)
|
return "\n".join(H)
|
||||||
|
|
||||||
|
|
||||||
def _get_sorted_etuds(evaluation: Evaluation, etudids: list, formsemestre_id: int):
|
def get_sorted_etuds_notes(
|
||||||
# Notes existantes
|
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)
|
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation.id)
|
||||||
cnx = ndb.GetDBConnexion()
|
cnx = ndb.GetDBConnexion()
|
||||||
etuds = []
|
etuds = []
|
||||||
@ -1182,7 +756,9 @@ def _get_sorted_etuds(evaluation: Evaluation, etudids: list, formsemestre_id: in
|
|||||||
|
|
||||||
# Note actuelle de l'étudiant:
|
# Note actuelle de l'étudiant:
|
||||||
if etudid in notes_db:
|
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"]
|
comment = notes_db[etudid]["comment"]
|
||||||
if comment is None:
|
if comment is None:
|
||||||
comment = ""
|
comment = ""
|
||||||
@ -1236,7 +812,7 @@ def _form_saisie_notes(
|
|||||||
# Nb de décisions de jury (pour les inscrits à l'évaluation):
|
# Nb de décisions de jury (pour les inscrits à l'évaluation):
|
||||||
nb_decisions = sum(decisions_jury.values())
|
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:
|
# Build form:
|
||||||
descr = [
|
descr = [
|
||||||
@ -1263,7 +839,7 @@ def _form_saisie_notes(
|
|||||||
"title": "Notes ",
|
"title": "Notes ",
|
||||||
"cssclass": "formnote_bareme",
|
"cssclass": "formnote_bareme",
|
||||||
"readonly": True,
|
"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"]:
|
for group_info in e["groups"]:
|
||||||
etud_classes.append("group-" + str(group_info["group_id"]))
|
etud_classes.append("group-" + str(group_info["group_id"]))
|
||||||
|
|
||||||
label = (
|
label = f"""<span class="{classdem}">{e["civilite_str"]} {
|
||||||
'<span class="%s">' % classdem
|
scu.format_nomprenom(e, reverse=True)}</span>"""
|
||||||
+ e["civilite_str"]
|
|
||||||
+ " "
|
|
||||||
+ scu.format_nomprenom(e, reverse=True)
|
|
||||||
+ "</span>"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Historique des saisies de notes:
|
# Historique des saisies de notes:
|
||||||
explanation = (
|
explanation = (
|
||||||
@ -1322,7 +893,7 @@ def _form_saisie_notes(
|
|||||||
|
|
||||||
# Valeur actuelle du champ:
|
# Valeur actuelle du champ:
|
||||||
initvalues["note_" + str(etudid)] = e["val"]
|
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:
|
# Element de formulaire:
|
||||||
descr.append(
|
descr.append(
|
||||||
@ -1334,11 +905,11 @@ def _form_saisie_notes(
|
|||||||
"explanation": explanation,
|
"explanation": explanation,
|
||||||
"return_focus_next": True,
|
"return_focus_next": True,
|
||||||
"attributes": [
|
"attributes": [
|
||||||
'class="note%s"' % classdem,
|
f'class="note{classdem}"',
|
||||||
disabled_attr,
|
disabled_attr,
|
||||||
'data-last-saved-value="%s"' % e["val"],
|
f'''data-last-saved-value="{e['val']}"''',
|
||||||
'data-orig-value="%s"' % e["val"],
|
f'''data-orig-value="{e["val"]}"''',
|
||||||
'data-etudid="%s"' % etudid,
|
f'data-etudid="{etudid}"',
|
||||||
],
|
],
|
||||||
"template": """<tr%(item_dom_attr)s class="etud_elem """
|
"template": """<tr%(item_dom_attr)s class="etud_elem """
|
||||||
+ " ".join(etud_classes)
|
+ " ".join(etud_classes)
|
||||||
@ -1422,7 +993,7 @@ def save_notes(
|
|||||||
if not evaluation.moduleimpl.can_edit_notes(current_user):
|
if not evaluation.moduleimpl.can_edit_notes(current_user):
|
||||||
return json_error(403, "modification notes non autorisee pour cet utilisateur")
|
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:
|
if valid_notes:
|
||||||
etudids_changed, _, etudids_with_decision, messages = notes_add(
|
etudids_changed, _, etudids_with_decision, messages = notes_add(
|
||||||
current_user, evaluation.id, valid_notes, comment=comment, do_it=True
|
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 = []
|
H = []
|
||||||
if len(history) > 1:
|
if len(history) > 1:
|
||||||
H.append(
|
H.append(
|
||||||
'<select data-etudid="%s" class="note_history" onchange="change_history(this);">'
|
f'<select data-etudid="{etudid}" class="note_history" onchange="change_history(this);">'
|
||||||
% etudid
|
|
||||||
)
|
)
|
||||||
envir = "select"
|
envir = "select"
|
||||||
item = "option"
|
item = "option"
|
||||||
@ -1477,21 +1047,19 @@ def get_note_history_menu(evaluation_id: int, etudid: int) -> str:
|
|||||||
|
|
||||||
first = True
|
first = True
|
||||||
for i in history:
|
for i in history:
|
||||||
jt = i["date"].strftime("le %d/%m/%Y à %H:%M") + " (%s)" % i["user_name"]
|
jt = f"""{i["date"].strftime("le %d/%m/%Y à %H:%M")} ({i["user_name"]})"""
|
||||||
dispnote = _display_note(i["value"])
|
dispnote = scu.fmt_note(i["value"], fixed_precision_str=False)
|
||||||
if first:
|
if first:
|
||||||
nv = "" # ne repete pas la valeur de la note courante
|
nv = "" # ne repete pas la valeur de la note courante
|
||||||
else:
|
else:
|
||||||
# ancienne valeur
|
# ancienne valeur
|
||||||
nv = ": %s" % dispnote
|
nv = f": {dispnote}"
|
||||||
first = False
|
first = False
|
||||||
if i["comment"]:
|
if i["comment"]:
|
||||||
comment = ' <span class="histcomment">%s</span>' % i["comment"]
|
comment = f' <span class="histcomment">{i["comment"]}</span>'
|
||||||
else:
|
else:
|
||||||
comment = ""
|
comment = ""
|
||||||
H.append(
|
H.append(f'<{item} data-note="{dispnote}">{jt} {nv}{comment}</{item}>')
|
||||||
'<%s data-note="%s">%s %s%s</%s>' % (item, dispnote, jt, nv, comment, item)
|
|
||||||
)
|
|
||||||
|
|
||||||
H.append("</%s>" % envir)
|
H.append(f"</{envir}>")
|
||||||
return "\n".join(H)
|
return "\n".join(H)
|
||||||
|
@ -560,9 +560,13 @@ DATEATIME_FMT = DATE_FMT + " à " + TIME_FMT
|
|||||||
DATETIME_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.
|
"""conversion note en str pour affichage dans tables HTML ou PDF.
|
||||||
Si keep_numeric, laisse les valeur numeriques telles quelles (pour export Excel)
|
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:
|
if val is None or val == NOTES_ABSENCE:
|
||||||
return "ABS"
|
return "ABS"
|
||||||
@ -570,6 +574,8 @@ def fmt_note(val, note_max=None, keep_numeric=False):
|
|||||||
return "EXC" # excuse, note neutralise
|
return "EXC" # excuse, note neutralise
|
||||||
if val == NOTES_ATTENTE:
|
if val == NOTES_ATTENTE:
|
||||||
return "ATT" # attente, note neutralisee
|
return "ATT" # attente, note neutralisee
|
||||||
|
if val == NOTES_SUPPRESS:
|
||||||
|
return "SUPR" # pour les formulaires de saisie
|
||||||
if not isinstance(val, str):
|
if not isinstance(val, str):
|
||||||
if np.isnan(val):
|
if np.isnan(val):
|
||||||
return "~"
|
return "~"
|
||||||
@ -577,11 +583,12 @@ def fmt_note(val, note_max=None, keep_numeric=False):
|
|||||||
val = val * 20.0 / note_max
|
val = val * 20.0 / note_max
|
||||||
if keep_numeric:
|
if keep_numeric:
|
||||||
return val
|
return val
|
||||||
else:
|
if fixed_precision_str:
|
||||||
s = "%2.2f" % round(float(val), 2) # 2 chiffres apres la virgule
|
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"
|
s = "0" * (5 - len(s)) + s # padding: 0 à gauche pour longueur 5: "12.34"
|
||||||
return s
|
return s
|
||||||
else:
|
return f"{val:g}"
|
||||||
|
|
||||||
return val.replace("NA", "-")
|
return val.replace("NA", "-")
|
||||||
|
|
||||||
|
|
||||||
|
@ -147,6 +147,7 @@ from app.scodoc import (
|
|||||||
sco_recapcomplet,
|
sco_recapcomplet,
|
||||||
sco_report,
|
sco_report,
|
||||||
sco_report_but,
|
sco_report_but,
|
||||||
|
sco_saisie_excel,
|
||||||
sco_saisie_notes,
|
sco_saisie_notes,
|
||||||
sco_semset,
|
sco_semset,
|
||||||
sco_synchro_etuds,
|
sco_synchro_etuds,
|
||||||
@ -1827,13 +1828,13 @@ sco_publish(
|
|||||||
# --- Saisie des notes
|
# --- Saisie des notes
|
||||||
sco_publish(
|
sco_publish(
|
||||||
"/saisie_notes_tableur",
|
"/saisie_notes_tableur",
|
||||||
sco_saisie_notes.saisie_notes_tableur,
|
sco_saisie_excel.saisie_notes_tableur,
|
||||||
Permission.EnsView,
|
Permission.EnsView,
|
||||||
methods=["GET", "POST"],
|
methods=["GET", "POST"],
|
||||||
)
|
)
|
||||||
sco_publish(
|
sco_publish(
|
||||||
"/feuille_saisie_notes",
|
"/feuille_saisie_notes",
|
||||||
sco_saisie_notes.feuille_saisie_notes,
|
sco_saisie_excel.feuille_saisie_notes,
|
||||||
Permission.EnsView,
|
Permission.EnsView,
|
||||||
)
|
)
|
||||||
sco_publish("/saisie_notes", sco_saisie_notes.saisie_notes, Permission.EnsView)
|
sco_publish("/saisie_notes", sco_saisie_notes.saisie_notes, Permission.EnsView)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# -*- mode: python -*-
|
# -*- mode: python -*-
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
SCOVERSION = "9.6.984"
|
SCOVERSION = "9.6.985"
|
||||||
|
|
||||||
SCONAME = "ScoDoc"
|
SCONAME = "ScoDoc"
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user