forked from ScoDoc/ScoDoc
Saisie notes multi-évaluations. Closes #942.
This commit is contained in:
parent
94a77abc92
commit
9b825c0fb1
@ -267,10 +267,12 @@ class Evaluation(models.ScoDocModel):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_evaluation(
|
def get_evaluation(
|
||||||
cls, evaluation_id: int | str, dept_id: int = None
|
cls, evaluation_id: int | str, dept_id: int = None, accept_none=False
|
||||||
) -> "Evaluation":
|
) -> "Evaluation":
|
||||||
"""Evaluation ou 404, cherche uniquement dans le département spécifié ou le courant."""
|
"""Evaluation ou 404, cherche uniquement dans le département spécifié ou le courant.
|
||||||
from app.models import FormSemestre, ModuleImpl
|
Si accept_none, return None si l'id est invalide ou n'existe pas.
|
||||||
|
"""
|
||||||
|
from app.models import FormSemestre
|
||||||
|
|
||||||
if not isinstance(evaluation_id, int):
|
if not isinstance(evaluation_id, int):
|
||||||
try:
|
try:
|
||||||
@ -282,6 +284,8 @@ class Evaluation(models.ScoDocModel):
|
|||||||
query = cls.query.filter_by(id=evaluation_id)
|
query = cls.query.filter_by(id=evaluation_id)
|
||||||
if dept_id is not None:
|
if dept_id is not None:
|
||||||
query = query.join(ModuleImpl).join(FormSemestre).filter_by(dept_id=dept_id)
|
query = query.join(ModuleImpl).join(FormSemestre).filter_by(dept_id=dept_id)
|
||||||
|
if accept_none:
|
||||||
|
return query.first()
|
||||||
return query.first_or_404()
|
return query.first_or_404()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -60,12 +60,12 @@ class COLORS(Enum):
|
|||||||
LIGHT_YELLOW = "FFFFFF99"
|
LIGHT_YELLOW = "FFFFFF99"
|
||||||
|
|
||||||
|
|
||||||
# Un style est enregistré comme un dictionnaire qui précise la valeur d'un attribut dans la liste suivante:
|
# Un style est enregistré comme un dictionnaire avec des attributs dans la liste suivante:
|
||||||
# font, border, number_format, fill,...
|
# font, border, number_format, fill,...
|
||||||
# (cf https://openpyxl.readthedocs.io/en/stable/styles.html#working-with-styles)
|
# (cf https://openpyxl.readthedocs.io/en/stable/styles.html#working-with-styles)
|
||||||
|
|
||||||
|
|
||||||
def xldate_as_datetime(xldate, datemode=0):
|
def xldate_as_datetime(xldate):
|
||||||
"""Conversion d'une date Excel en datetime python
|
"""Conversion d'une date Excel en datetime python
|
||||||
Deux formats de chaîne acceptés:
|
Deux formats de chaîne acceptés:
|
||||||
* JJ/MM/YYYY (chaîne naïve)
|
* JJ/MM/YYYY (chaîne naïve)
|
||||||
@ -187,8 +187,8 @@ def excel_make_style(
|
|||||||
|
|
||||||
class ScoExcelSheet:
|
class ScoExcelSheet:
|
||||||
"""Représente une feuille qui peut être indépendante ou intégrée dans un ScoExcelBook.
|
"""Représente une feuille qui peut être indépendante ou intégrée dans un ScoExcelBook.
|
||||||
En application des directives de la bibliothèque sur l'écriture optimisée, l'ordre des opérations
|
En application des directives de la bibliothèque sur l'écriture optimisée,
|
||||||
est imposé:
|
l'ordre des opérations est imposé:
|
||||||
* instructions globales (largeur/maquage des colonnes et ligne, ...)
|
* instructions globales (largeur/maquage des colonnes et ligne, ...)
|
||||||
* construction et ajout des cellules et ligne selon le sens de lecture (occidental)
|
* construction et ajout des cellules et ligne selon le sens de lecture (occidental)
|
||||||
ligne de haut en bas et cellules de gauche à droite (i.e. A1, A2, .. B1, B2, ..)
|
ligne de haut en bas et cellules de gauche à droite (i.e. A1, A2, .. B1, B2, ..)
|
||||||
@ -199,7 +199,7 @@ class ScoExcelSheet:
|
|||||||
"""Création de la feuille. sheet_name
|
"""Création de la feuille. sheet_name
|
||||||
-- le nom de la feuille default_style
|
-- le nom de la feuille default_style
|
||||||
-- le style par défaut des cellules ws
|
-- le style par défaut des cellules ws
|
||||||
-- None si la feuille est autonome (dans ce cas elle crée son propre wb), sinon c'est la worksheet
|
-- None si la feuille est autonome (elle crée son propre wb), sinon c'est la worksheet
|
||||||
créée par le workbook propriétaire un workbook est créé et associé à cette feuille.
|
créée par le workbook propriétaire un workbook est créé et associé à cette feuille.
|
||||||
"""
|
"""
|
||||||
# Le nom de la feuille ne peut faire plus de 31 caractères.
|
# Le nom de la feuille ne peut faire plus de 31 caractères.
|
||||||
@ -228,7 +228,8 @@ class ScoExcelSheet:
|
|||||||
fill=None,
|
fill=None,
|
||||||
number_format=None,
|
number_format=None,
|
||||||
font=None,
|
font=None,
|
||||||
):
|
) -> dict:
|
||||||
|
"création d'un dict"
|
||||||
style = {}
|
style = {}
|
||||||
if font is not None:
|
if font is not None:
|
||||||
style["font"] = font
|
style["font"] = font
|
||||||
@ -393,7 +394,7 @@ class ScoExcelSheet:
|
|||||||
if isinstance(value, datetime.date):
|
if isinstance(value, datetime.date):
|
||||||
cell.data_type = "d"
|
cell.data_type = "d"
|
||||||
cell.number_format = FORMAT_DATE_DDMMYY
|
cell.number_format = FORMAT_DATE_DDMMYY
|
||||||
elif isinstance(value, int) or isinstance(value, float):
|
elif isinstance(value, (int, float)):
|
||||||
cell.data_type = "n"
|
cell.data_type = "n"
|
||||||
else:
|
else:
|
||||||
cell.data_type = "s"
|
cell.data_type = "s"
|
||||||
@ -432,10 +433,11 @@ class ScoExcelSheet:
|
|||||||
Ce flux pourra ensuite être repris dans send_excel_file (classeur mono feille)
|
Ce flux pourra ensuite être repris dans send_excel_file (classeur mono feille)
|
||||||
ou pour la génération d'un classeur multi-feuilles
|
ou pour la génération d'un classeur multi-feuilles
|
||||||
"""
|
"""
|
||||||
for row in self.column_dimensions.keys():
|
for k, v in self.column_dimensions.items():
|
||||||
self.ws.column_dimensions[row] = self.column_dimensions[row]
|
self.ws.column_dimensions[k] = v
|
||||||
for row in self.row_dimensions.keys():
|
|
||||||
self.ws.row_dimensions[row] = self.row_dimensions[row]
|
for k, v in self.row_dimensions.items():
|
||||||
|
self.ws.row_dimensions[k] = self.row_dimensions[v]
|
||||||
for row in self.rows:
|
for row in self.rows:
|
||||||
self.ws.append(row)
|
self.ws.append(row)
|
||||||
|
|
||||||
@ -529,17 +531,6 @@ def excel_file_to_list(filename):
|
|||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
def excel_workbook_to_list(filename):
|
|
||||||
try:
|
|
||||||
return _excel_workbook_to_list(filename)
|
|
||||||
except Exception as exc:
|
|
||||||
raise ScoValueError(
|
|
||||||
"""Le fichier xlsx attendu n'est pas lisible !
|
|
||||||
Peut-être avez-vous fourni un fichier au mauvais format (txt, xls, ...)
|
|
||||||
"""
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
|
|
||||||
def _open_workbook(filelike, dump_debug=False) -> Workbook:
|
def _open_workbook(filelike, dump_debug=False) -> Workbook:
|
||||||
"""Open document.
|
"""Open document.
|
||||||
On error, if dump-debug is True, dump data in /tmp for debugging purpose
|
On error, if dump-debug is True, dump data in /tmp for debugging purpose
|
||||||
@ -559,7 +550,7 @@ def _open_workbook(filelike, dump_debug=False) -> Workbook:
|
|||||||
return workbook
|
return workbook
|
||||||
|
|
||||||
|
|
||||||
def _excel_to_list(filelike):
|
def _excel_to_list(filelike) -> tuple[list, list[list]]:
|
||||||
"""returns list of list"""
|
"""returns list of list"""
|
||||||
workbook = _open_workbook(filelike)
|
workbook = _open_workbook(filelike)
|
||||||
diag = [] # liste de chaines pour former message d'erreur
|
diag = [] # liste de chaines pour former message d'erreur
|
||||||
@ -576,7 +567,7 @@ def _excel_to_list(filelike):
|
|||||||
return diag, matrix
|
return diag, matrix
|
||||||
|
|
||||||
|
|
||||||
def _excel_sheet_to_list(sheet: Worksheet, sheet_name: str) -> tuple[list, list]:
|
def _excel_sheet_to_list(sheet: Worksheet, sheet_name: str) -> tuple[list, list[list]]:
|
||||||
"""read a spreadsheet sheet, and returns:
|
"""read a spreadsheet sheet, and returns:
|
||||||
- diag : a list of strings (error messages aimed at helping the user)
|
- diag : a list of strings (error messages aimed at helping the user)
|
||||||
- a list of lists: the spreadsheet cells
|
- a list of lists: the spreadsheet cells
|
||||||
@ -609,14 +600,21 @@ def _excel_sheet_to_list(sheet: Worksheet, sheet_name: str) -> tuple[list, list]
|
|||||||
return diag, matrix
|
return diag, matrix
|
||||||
|
|
||||||
|
|
||||||
def _excel_workbook_to_list(filelike):
|
def excel_workbook_to_list(filelike):
|
||||||
"""Lit un classeur (workbook): chaque feuille est lue
|
"""Lit un classeur (workbook): chaque feuille est lue
|
||||||
et est convertie en une liste de listes.
|
et est convertie en une liste de listes.
|
||||||
Returns:
|
Returns:
|
||||||
- diag : a list of strings (error messages aimed at helping the user)
|
- diag : a list of strings (error messages aimed at helping the user)
|
||||||
- a list of lists: the spreadsheet cells
|
- a list of lists: the spreadsheet cells
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
workbook = _open_workbook(filelike)
|
workbook = _open_workbook(filelike)
|
||||||
|
except Exception as exc:
|
||||||
|
raise ScoValueError(
|
||||||
|
"""Le fichier xlsx attendu n'est pas lisible !
|
||||||
|
Peut-être avez-vous fourni un fichier au mauvais format (txt, xls, ...)
|
||||||
|
"""
|
||||||
|
) from exc
|
||||||
diag = [] # liste de chaines pour former message d'erreur
|
diag = [] # liste de chaines pour former message d'erreur
|
||||||
if len(workbook.sheetnames) < 1:
|
if len(workbook.sheetnames) < 1:
|
||||||
diag.append("Aucune feuille trouvée dans le classeur !")
|
diag.append("Aucune feuille trouvée dans le classeur !")
|
||||||
@ -631,6 +629,7 @@ def _excel_workbook_to_list(filelike):
|
|||||||
return diag, matrix_list
|
return diag, matrix_list
|
||||||
|
|
||||||
|
|
||||||
|
# TODO déplacer dans un autre fichier
|
||||||
def excel_feuille_listeappel(
|
def excel_feuille_listeappel(
|
||||||
sem,
|
sem,
|
||||||
groupname,
|
groupname,
|
||||||
|
@ -23,6 +23,21 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
"""Fichier excel de saisie des notes
|
"""Fichier excel de saisie des notes
|
||||||
|
|
||||||
|
|
||||||
|
## Notes d'une évaluation
|
||||||
|
|
||||||
|
saisie_notes_tableur (formulaire)
|
||||||
|
-> feuille_saisie_notes (génération de l'excel)
|
||||||
|
-> do_evaluations_upload_xls
|
||||||
|
|
||||||
|
## Notes d'un semestre
|
||||||
|
|
||||||
|
formsemestre_import_notes (formulaire, import_notes.j2)
|
||||||
|
-> feuille_import_notes (génération de l'excel)
|
||||||
|
-> formsemestre_import_notes
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import AnyStr
|
from typing import AnyStr
|
||||||
@ -30,13 +45,14 @@ from typing import AnyStr
|
|||||||
from openpyxl.styles import Alignment, Border, Color, Font, PatternFill, Side
|
from openpyxl.styles import Alignment, Border, Color, Font, PatternFill, Side
|
||||||
from openpyxl.styles.numbers import FORMAT_GENERAL
|
from openpyxl.styles.numbers import FORMAT_GENERAL
|
||||||
|
|
||||||
from flask import g, request, url_for
|
from flask import g, render_template, request, url_for
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
|
||||||
from app.models import Evaluation, FormSemestre, Identite, Module, ScolarNews
|
from app.models import Evaluation, FormSemestre, Identite, ScolarNews
|
||||||
from app.scodoc.sco_excel import COLORS, ScoExcelSheet
|
from app.scodoc.sco_excel import COLORS, ScoExcelSheet
|
||||||
from app.scodoc import (
|
from app.scodoc import (
|
||||||
html_sco_header,
|
html_sco_header,
|
||||||
|
sco_cache,
|
||||||
sco_evaluations,
|
sco_evaluations,
|
||||||
sco_evaluation_db,
|
sco_evaluation_db,
|
||||||
sco_excel,
|
sco_excel,
|
||||||
@ -48,6 +64,7 @@ from app.scodoc import (
|
|||||||
from app.scodoc.sco_exceptions import AccessDenied, InvalidNoteValue
|
from app.scodoc.sco_exceptions import AccessDenied, InvalidNoteValue
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||||
|
from app.views import ScoData
|
||||||
|
|
||||||
FONT_NAME = "Arial"
|
FONT_NAME = "Arial"
|
||||||
|
|
||||||
@ -180,7 +197,7 @@ def _insert_line_titles(
|
|||||||
ws.append_row(cells)
|
ws.append_row(cells)
|
||||||
|
|
||||||
# Calcul largeur colonnes (actuellement pour feuille import multi seulement)
|
# Calcul largeur colonnes (actuellement pour feuille import multi seulement)
|
||||||
# Le facteur prend en compte la tailel du font (14)
|
# Le facteur prend en compte la taille du font (14)
|
||||||
font_size_factor = 1.25
|
font_size_factor = 1.25
|
||||||
column_widths = {
|
column_widths = {
|
||||||
ScoExcelSheet.i2col(idx): (len(str(cell.value)) + 2.0) * font_size_factor
|
ScoExcelSheet.i2col(idx): (len(str(cell.value)) + 2.0) * font_size_factor
|
||||||
@ -525,36 +542,113 @@ def generate_excel_import_notes(
|
|||||||
return ws.generate(column_widths=column_widths)
|
return ws.generate(column_widths=column_widths)
|
||||||
|
|
||||||
|
|
||||||
def do_evaluation_upload_xls() -> tuple[bool, str]:
|
def do_evaluations_upload_xls(
|
||||||
|
notefile,
|
||||||
|
comment: str = "",
|
||||||
|
evaluation: Evaluation | None = None,
|
||||||
|
formsemestre: FormSemestre | None = None,
|
||||||
|
) -> tuple[bool, str]:
|
||||||
"""
|
"""
|
||||||
Soumission d'un fichier XLS (evaluation_id, notefile)
|
Soumission d'un fichier XLS (evaluation_id, notefile)
|
||||||
|
soit dans le formsemestre (import multi-eval)
|
||||||
|
soit dans une seule évaluation
|
||||||
return:
|
return:
|
||||||
ok: bool
|
ok: bool
|
||||||
msg: message diagonistic à affciher
|
msg: message diagnostic à affciher
|
||||||
"""
|
"""
|
||||||
args = scu.get_request_args()
|
diag, rows = sco_excel.excel_file_to_list(notefile)
|
||||||
comment = args["comment"]
|
|
||||||
evaluation = Evaluation.get_evaluation(args["evaluation_id"])
|
|
||||||
|
|
||||||
# Check access (admin, respformation, responsable_id, ens)
|
|
||||||
if not evaluation.moduleimpl.can_edit_notes(current_user):
|
|
||||||
raise AccessDenied(f"Modification des notes impossible pour {current_user}")
|
|
||||||
#
|
|
||||||
diag, rows = sco_excel.excel_file_to_list(args["notefile"])
|
|
||||||
try:
|
try:
|
||||||
if not rows:
|
if not rows:
|
||||||
raise InvalidNoteValue()
|
raise InvalidNoteValue()
|
||||||
|
|
||||||
|
# Lecture des évaluations ids
|
||||||
row_title_idx, evaluations, evaluations_col_idx = _get_sheet_evaluations(
|
row_title_idx, evaluations, evaluations_col_idx = _get_sheet_evaluations(
|
||||||
rows, evaluation=evaluation, diag=diag
|
rows, evaluation=evaluation, formsemestre=formsemestre, diag=diag
|
||||||
)
|
)
|
||||||
# --- get notes -> list (etudid, value)
|
|
||||||
# ignore toutes les lignes ne commençant pas par !
|
# Vérification des permissions (admin, resp. formation, responsable_id, ens)
|
||||||
notes_by_eval = defaultdict(
|
for e in evaluations:
|
||||||
list
|
if not e.moduleimpl.can_edit_notes(current_user):
|
||||||
) # { evaluation_id : [ (etudid, note_value), ... ] }
|
raise AccessDenied(
|
||||||
ni = row_title_idx + 1
|
f"""Modification des notes
|
||||||
for row in rows[row_title_idx + 1 :]:
|
dans le module {e.moduleimpl.module.code}
|
||||||
|
impossible pour {current_user}"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Lecture des notes
|
||||||
|
notes_by_eval = _read_notes_from_rows(
|
||||||
|
rows, diag, evaluations, evaluations_col_idx, start=row_title_idx + 1
|
||||||
|
)
|
||||||
|
|
||||||
|
# -- Enregistre les notes de chaque évaluation
|
||||||
|
with sco_cache.DeferredSemCacheManager():
|
||||||
|
messages_by_eval, etudids_with_decisions = _record_notes_evaluations(
|
||||||
|
evaluations, notes_by_eval, comment, diag, rows=rows
|
||||||
|
)
|
||||||
|
|
||||||
|
# -- News
|
||||||
|
if len(evaluations) > 1:
|
||||||
|
modules_str = ", ".join(
|
||||||
|
[evaluation.moduleimpl.module.code for evaluation in evaluations]
|
||||||
|
)
|
||||||
|
status_url = url_for(
|
||||||
|
"notes.formsemestre_status",
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
formsemestre_id=formsemestre.id,
|
||||||
|
)
|
||||||
|
obj_id = formsemestre.id
|
||||||
|
else:
|
||||||
|
modules_str = (
|
||||||
|
evaluation.moduleimpl.module.titre or evaluation.moduleimpl.module.code
|
||||||
|
)
|
||||||
|
status_url = url_for(
|
||||||
|
"notes.moduleimpl_status",
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
moduleimpl_id=evaluation.moduleimpl_id,
|
||||||
|
)
|
||||||
|
obj_id = evaluation.moduleimpl_id
|
||||||
|
ScolarNews.add(
|
||||||
|
typ=ScolarNews.NEWS_NOTE,
|
||||||
|
obj=obj_id,
|
||||||
|
text=f"""Chargement notes dans <a href="{status_url}">{modules_str}</a>""",
|
||||||
|
url=status_url,
|
||||||
|
max_frequency=30 * 60, # 30 minutes
|
||||||
|
)
|
||||||
|
|
||||||
|
msg = "<div>" + "\n".join(messages_by_eval.values()) + "</div>"
|
||||||
|
if etudids_with_decisions:
|
||||||
|
msg = (
|
||||||
|
"""<p class="warning"><b>Important:</b>
|
||||||
|
Il y avait déjà des décisions de jury
|
||||||
|
enregistrées, qui sont à revoir suite à cette modification !
|
||||||
|
</p>
|
||||||
|
"""
|
||||||
|
+ msg
|
||||||
|
)
|
||||||
|
return True, 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 False, msg + "<p>(pas de notes modifiées)</p>"
|
||||||
|
|
||||||
|
|
||||||
|
def _read_notes_from_rows(
|
||||||
|
rows: list[list], diag, evaluations, evaluations_col_idx, start=0
|
||||||
|
):
|
||||||
|
"""--- get notes -> list (etudid, value)
|
||||||
|
ignore toutes les lignes ne commençant pas par '!'
|
||||||
|
"""
|
||||||
|
# { evaluation_id : [ (etudid, note_value), ... ] }
|
||||||
|
notes_by_eval = defaultdict(list)
|
||||||
|
ni = start
|
||||||
|
for row in rows[start:]:
|
||||||
if row:
|
if row:
|
||||||
cell0 = row[0].strip()
|
cell0 = row[0].strip()
|
||||||
if cell0 and cell0[0] == "!":
|
if cell0 and cell0[0] == "!":
|
||||||
@ -575,12 +669,20 @@ def do_evaluation_upload_xls() -> tuple[bool, str]:
|
|||||||
)
|
)
|
||||||
ni += 1
|
ni += 1
|
||||||
|
|
||||||
|
return notes_by_eval
|
||||||
|
|
||||||
|
|
||||||
|
def _record_notes_evaluations(
|
||||||
|
evaluations, notes_by_eval, comment, diag, rows: list[list[str]] | None = None
|
||||||
|
) -> tuple[dict[int, str], set[int]]:
|
||||||
|
"""Enregistre les notes dans les évaluations
|
||||||
|
Return: messages_by_eval, etudids_with_decisions
|
||||||
|
"""
|
||||||
# -- Check values de chaque évaluation
|
# -- Check values de chaque évaluation
|
||||||
valid_notes_by_eval, etudids_without_notes_by_eval, etudids_absents_by_eval = (
|
valid_notes_by_eval, etudids_without_notes_by_eval, etudids_absents_by_eval = (
|
||||||
_check_notes_evaluations(evaluations, notes_by_eval, diag)
|
_check_notes_evaluations(evaluations, notes_by_eval, diag, rows=rows)
|
||||||
)
|
)
|
||||||
|
|
||||||
# -- Enregistre les notes de chaque évaluation
|
|
||||||
messages_by_eval: dict[int, str] = {}
|
messages_by_eval: dict[int, str] = {}
|
||||||
etudids_with_decisions = set()
|
etudids_with_decisions = set()
|
||||||
for evaluation in evaluations:
|
for evaluation in evaluations:
|
||||||
@ -589,17 +691,18 @@ def do_evaluation_upload_xls() -> tuple[bool, str]:
|
|||||||
continue
|
continue
|
||||||
etudids_changed, nb_suppress, etudids_with_decisions_eval, messages = (
|
etudids_changed, nb_suppress, etudids_with_decisions_eval, messages = (
|
||||||
sco_saisie_notes.notes_add(
|
sco_saisie_notes.notes_add(
|
||||||
current_user,
|
current_user, evaluation.id, valid_notes, comment
|
||||||
evaluation.id,
|
|
||||||
valid_notes_by_eval[evaluation.id],
|
|
||||||
comment,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
etudids_with_decisions |= set(etudids_with_decisions_eval)
|
etudids_with_decisions |= set(etudids_with_decisions_eval)
|
||||||
msg = f"""<div class="diag-evaluation">
|
msg = f"""<div class="diag-evaluation">
|
||||||
<ul>
|
<ul>
|
||||||
<li><div>Module {evaluation.moduleimpl.module.code} :
|
<li><div class="{'diag-change' if etudids_changed else 'diag-nochange'}">
|
||||||
|
Module {evaluation.moduleimpl.module.code} :
|
||||||
évaluation {evaluation.description} {evaluation.descr_date()}
|
évaluation {evaluation.description} {evaluation.descr_date()}
|
||||||
|
"""
|
||||||
|
msg += (
|
||||||
|
f"""
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{len(etudids_changed)} notes changées
|
{len(etudids_changed)} notes changées
|
||||||
@ -607,9 +710,11 @@ def do_evaluation_upload_xls() -> tuple[bool, str]:
|
|||||||
{len(etudids_absents_by_eval[evaluation.id])} absents,
|
{len(etudids_absents_by_eval[evaluation.id])} absents,
|
||||||
{nb_suppress} note supprimées)
|
{nb_suppress} note supprimées)
|
||||||
</div>
|
</div>
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
"""
|
"""
|
||||||
|
if etudids_changed
|
||||||
|
else " : pas de changement</div>"
|
||||||
|
)
|
||||||
|
msg += "</li></ul>"
|
||||||
if messages:
|
if messages:
|
||||||
msg += f"""<div class="warning">Attention :
|
msg += f"""<div class="warning">Attention :
|
||||||
<ul>
|
<ul>
|
||||||
@ -621,46 +726,14 @@ def do_evaluation_upload_xls() -> tuple[bool, str]:
|
|||||||
</div>"""
|
</div>"""
|
||||||
msg += """</div>"""
|
msg += """</div>"""
|
||||||
messages_by_eval[evaluation.id] = msg
|
messages_by_eval[evaluation.id] = msg
|
||||||
|
return messages_by_eval, etudids_with_decisions
|
||||||
# -- 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
|
|
||||||
)
|
|
||||||
|
|
||||||
if etudids_with_decisions:
|
|
||||||
msg += """<p class="warning"><b>Important:</b> il y avait déjà des décisions de jury
|
|
||||||
enregistrées, qui sont à revoir suite à cette modification !</p>
|
|
||||||
"""
|
|
||||||
return True, 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 False, msg + "<p>(pas de notes modifiées)</p>"
|
|
||||||
|
|
||||||
|
|
||||||
def _check_notes_evaluations(
|
def _check_notes_evaluations(
|
||||||
evaluations: list[Evaluation],
|
evaluations: list[Evaluation],
|
||||||
notes_by_eval: dict[int, list[tuple[int, str]]],
|
notes_by_eval: dict[int, list[tuple[int, str]]],
|
||||||
diag: list[str],
|
diag: list[str],
|
||||||
|
rows: list[list[str]] | None = None,
|
||||||
) -> tuple[dict[int, list[tuple[int, str]]], list[int], list[int]]:
|
) -> tuple[dict[int, list[tuple[int, str]]], list[int], list[int]]:
|
||||||
"""Vérifie que les notes pour ces évaluations sont valides.
|
"""Vérifie que les notes pour ces évaluations sont valides.
|
||||||
Raise InvalidNoteValue et rempli diag si ce n'est pas le cas.
|
Raise InvalidNoteValue et rempli diag si ce n'est pas le cas.
|
||||||
@ -678,29 +751,40 @@ def _check_notes_evaluations(
|
|||||||
etudids_non_inscrits,
|
etudids_non_inscrits,
|
||||||
) = sco_saisie_notes.check_notes(notes_by_eval[evaluation.id], evaluation)
|
) = sco_saisie_notes.check_notes(notes_by_eval[evaluation.id], evaluation)
|
||||||
if invalids:
|
if invalids:
|
||||||
diag.append(
|
diag.append(f"Erreur: la feuille contient {len(invalids)} notes invalides")
|
||||||
f"Erreur: la feuille contient {len(invalids)} notes invalides</p>"
|
msg = f"""Notes invalides dans {
|
||||||
)
|
evaluation.moduleimpl.module.code} {evaluation.description} pour : """
|
||||||
if len(invalids) < 25:
|
if len(invalids) < 25:
|
||||||
etudsnames = [
|
etudsnames = [
|
||||||
Identite.get_etud(etudid).nom_prenom() for etudid in invalids
|
Identite.get_etud(etudid).nom_prenom() for etudid in invalids
|
||||||
]
|
]
|
||||||
diag.append("Notes invalides pour: " + ", ".join(etudsnames))
|
msg += ", ".join(etudsnames)
|
||||||
else:
|
else:
|
||||||
diag.append("Notes invalides pour plus de 25 étudiants")
|
msg += "plus de 25 étudiants"
|
||||||
|
diag.append(msg)
|
||||||
raise InvalidNoteValue()
|
raise InvalidNoteValue()
|
||||||
if etudids_non_inscrits:
|
if etudids_non_inscrits:
|
||||||
|
msg = ""
|
||||||
|
if len(etudids_non_inscrits) < 25:
|
||||||
|
# retrouve numéro ligne et données invalides dans fichier
|
||||||
|
for etudid in etudids_non_inscrits:
|
||||||
|
try:
|
||||||
|
index = [row[0] for row in rows].index(f"!{etudid}")
|
||||||
|
except ValueError:
|
||||||
|
index = None
|
||||||
|
msg += f"""<li>Ligne {index+1}:
|
||||||
|
{rows[index][1]} {rows[index][2]} (id={rows[index][0]})
|
||||||
|
</li>"""
|
||||||
|
else:
|
||||||
|
msg += "<li>sur plus de 25 lignes</li>"
|
||||||
diag.append(
|
diag.append(
|
||||||
f"""Erreur: la feuille contient {len(etudids_non_inscrits)
|
f"""Erreur: la feuille contient {len(etudids_non_inscrits)
|
||||||
} étudiants non inscrits</p>"""
|
} étudiants inexistants ou non inscrits à l'évaluation
|
||||||
|
{evaluation.moduleimpl.module.code}
|
||||||
|
{evaluation.description}
|
||||||
|
<ul>{msg}</ul>
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
if len(etudids_non_inscrits) < 25:
|
|
||||||
diag.append(
|
|
||||||
"etudid invalides (inexistants ou non inscrits): "
|
|
||||||
+ ", ".join(str(etudid) for etudid in etudids_non_inscrits)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
diag.append("etudid invalides sur plus de 25 lignes")
|
|
||||||
raise InvalidNoteValue()
|
raise InvalidNoteValue()
|
||||||
return valid_notes_by_eval, etudids_without_notes_by_eval, etudids_absents_by_eval
|
return valid_notes_by_eval, etudids_without_notes_by_eval, etudids_absents_by_eval
|
||||||
|
|
||||||
@ -724,8 +808,61 @@ def _read_notes_evaluations(
|
|||||||
notes_by_eval[evaluation.id].append((etud.id, val))
|
notes_by_eval[evaluation.id].append((etud.id, val))
|
||||||
|
|
||||||
|
|
||||||
|
def _xls_search_sheet_code(
|
||||||
|
rows: list[list[str]], diag: list[str] = None
|
||||||
|
) -> tuple[int, int | dict[int, int]]:
|
||||||
|
"""Cherche dans la feuille (liste de listes de chaines)
|
||||||
|
la ligne identifiant la ou les évaluations.
|
||||||
|
Si MULTIEVAL (import de plusieurs), renvoie
|
||||||
|
- l'indice de la ligne des TITRES
|
||||||
|
- un dict evaluations_col_idx: { evaluation_id : indice de sa colonne dans la feuille }
|
||||||
|
Si une seule éval (chargement dans une évaluation)
|
||||||
|
- l'indice de la ligne des TITRES
|
||||||
|
- l'evaluation_id indiqué dans la feuille
|
||||||
|
"""
|
||||||
|
for i, row in enumerate(rows):
|
||||||
|
if not row:
|
||||||
|
diag.append("Erreur: feuille invalide (ligne vide ?)")
|
||||||
|
raise InvalidNoteValue()
|
||||||
|
eval_code = row[0].strip()
|
||||||
|
# -- eval code: first cell in 1st column beginning by "!"
|
||||||
|
if eval_code.startswith("!"): # code évaluation trouvé
|
||||||
|
try:
|
||||||
|
sheet_eval_id = int(eval_code[1:])
|
||||||
|
except ValueError as exc:
|
||||||
|
diag.append("Erreur: feuille invalide ! (code évaluation invalide)")
|
||||||
|
raise InvalidNoteValue() from exc
|
||||||
|
return i, sheet_eval_id
|
||||||
|
|
||||||
|
# -- Muti-évaluation: les codes sont au dessus des titres
|
||||||
|
elif eval_code == "MULTIEVAL": # feuille import multi-eval
|
||||||
|
# cherche les ids des évaluations sur la même ligne
|
||||||
|
try:
|
||||||
|
evaluation_ids = [int(x) for x in row[4:]]
|
||||||
|
except ValueError as exc:
|
||||||
|
diag.append(
|
||||||
|
f"Erreur: feuille invalide ! (code évaluation invalide sur ligne {i+1})"
|
||||||
|
)
|
||||||
|
raise InvalidNoteValue() from exc
|
||||||
|
|
||||||
|
evaluations_col_idx = {
|
||||||
|
evaluation_id: j
|
||||||
|
for (j, evaluation_id) in enumerate(evaluation_ids, start=4)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
i + 1, # i+1 car MULTIEVAL sur la ligne précédent les titres
|
||||||
|
evaluations_col_idx,
|
||||||
|
)
|
||||||
|
|
||||||
|
diag.append("Erreur: feuille invalide ! (pas de ligne code évaluation)")
|
||||||
|
raise InvalidNoteValue()
|
||||||
|
|
||||||
|
|
||||||
def _get_sheet_evaluations(
|
def _get_sheet_evaluations(
|
||||||
rows: list[list[str]], evaluation: Evaluation | None = None, diag: list[str] = None
|
rows: list[list[str]],
|
||||||
|
evaluation: Evaluation | None = None,
|
||||||
|
formsemestre: FormSemestre | None = None,
|
||||||
|
diag: list[str] = None,
|
||||||
) -> tuple[int, list[Evaluation], dict[int, int]]:
|
) -> tuple[int, list[Evaluation], dict[int, int]]:
|
||||||
"""
|
"""
|
||||||
rows: les valeurs (str) des cellules de la feuille
|
rows: les valeurs (str) des cellules de la feuille
|
||||||
@ -735,35 +872,38 @@ def _get_sheet_evaluations(
|
|||||||
formsemestre ou evaluation doivent être indiqués.
|
formsemestre ou evaluation doivent être indiqués.
|
||||||
|
|
||||||
Résultat:
|
Résultat:
|
||||||
row_title_idx: l'indice (à partir de 0) de la ligne titre (après laquelle commencent les notes)
|
row_title_idx: l'indice (à partir de 0) de la ligne TITRE (après laquelle commencent les notes)
|
||||||
evaluations: liste des évaluations à remplir
|
evaluations: liste des évaluations à remplir
|
||||||
evaluations_col_idx: { evaluation_id : indice de sa colonne dans la feuille }
|
evaluations_col_idx: { evaluation_id : indice de sa colonne dans la feuille }
|
||||||
"""
|
"""
|
||||||
# -- search eval code: first cell in 1st column beginning by "!"
|
|
||||||
eval_code = None
|
|
||||||
for i, row in enumerate(rows):
|
|
||||||
if not row:
|
|
||||||
diag.append("Erreur: format invalide (ligne vide ?)")
|
|
||||||
raise InvalidNoteValue()
|
|
||||||
eval_code = row[0].strip()
|
|
||||||
if eval_code.startswith("!"):
|
|
||||||
break
|
|
||||||
if not eval_code:
|
|
||||||
diag.append("Erreur: format invalide ! (pas de ligne evaluation_id)")
|
|
||||||
raise InvalidNoteValue()
|
|
||||||
|
|
||||||
try:
|
i, r = _xls_search_sheet_code(rows, diag)
|
||||||
sheet_eval_id = int(eval_code[1:])
|
if isinstance(r, int): # mono-eval
|
||||||
except ValueError:
|
sheet_eval_id = r
|
||||||
sheet_eval_id = None
|
|
||||||
if sheet_eval_id != evaluation.id:
|
if sheet_eval_id != evaluation.id:
|
||||||
diag.append(
|
diag.append(
|
||||||
f"""Erreur: fichier invalide: le code d'évaluation de correspond pas ! ('{
|
f"""Erreur: fichier invalide: le code d'évaluation de correspond pas ! ('{
|
||||||
sheet_eval_id or ('non trouvé')}' != '{evaluation.id}')"""
|
sheet_eval_id or ('non trouvé')}' != '{evaluation.id}')"""
|
||||||
)
|
)
|
||||||
raise InvalidNoteValue()
|
raise InvalidNoteValue()
|
||||||
|
|
||||||
return i, [evaluation], {evaluation.id: 4}
|
return i, [evaluation], {evaluation.id: 4}
|
||||||
|
if isinstance(r, dict): # multi-eval
|
||||||
|
evaluations = []
|
||||||
|
evaluations_col_idx = r
|
||||||
|
# Load and check evaluations
|
||||||
|
for evaluation_id in evaluations_col_idx:
|
||||||
|
evaluation = Evaluation.get_evaluation(evaluation_id, accept_none=True)
|
||||||
|
if evaluation is None:
|
||||||
|
diag.append(f"""Erreur: l'évaluation {evaluation_id} n'existe pas""")
|
||||||
|
raise InvalidNoteValue()
|
||||||
|
if evaluation.moduleimpl.formsemestre_id != formsemestre.id:
|
||||||
|
diag.append(
|
||||||
|
f"""Erreur: l'évaluation {evaluation_id} n'existe pas dans ce semestre"""
|
||||||
|
)
|
||||||
|
raise InvalidNoteValue()
|
||||||
|
evaluations.append(evaluation)
|
||||||
|
return i, evaluations, evaluations_col_idx
|
||||||
|
raise ValueError("_get_sheet_evaluations")
|
||||||
|
|
||||||
|
|
||||||
def saisie_notes_tableur(evaluation_id: int, group_ids=()):
|
def saisie_notes_tableur(evaluation_id: int, group_ids=()):
|
||||||
@ -840,7 +980,7 @@ def saisie_notes_tableur(evaluation_id: int, group_ids=()):
|
|||||||
<span class="titredivsaisienote">Étape 2 : chargement d'un fichier de notes</span>""" # '
|
<span class="titredivsaisienote">Étape 2 : chargement d'un fichier de notes</span>""" # '
|
||||||
)
|
)
|
||||||
|
|
||||||
nf = TrivialFormulator(
|
tf = TrivialFormulator(
|
||||||
request.base_url,
|
request.base_url,
|
||||||
scu.get_request_args(),
|
scu.get_request_args(),
|
||||||
(
|
(
|
||||||
@ -861,23 +1001,27 @@ def saisie_notes_tableur(evaluation_id: int, group_ids=()):
|
|||||||
formid="notesfile",
|
formid="notesfile",
|
||||||
submitlabel="Télécharger",
|
submitlabel="Télécharger",
|
||||||
)
|
)
|
||||||
if nf[0] == 0:
|
if tf[0] == 0:
|
||||||
H.append(
|
H.append(
|
||||||
"""<p>Le fichier doit être un fichier tableur obtenu via
|
"""<p>Le fichier doit être un fichier tableur obtenu via
|
||||||
l'étape 1 ci-dessus, puis complété et enregistré au format Excel.
|
l'étape 1 ci-dessus, puis complété et enregistré au format Excel.
|
||||||
</p>"""
|
</p>"""
|
||||||
)
|
)
|
||||||
H.append(nf[1])
|
H.append(tf[1])
|
||||||
elif nf[0] == -1:
|
elif tf[0] == -1:
|
||||||
H.append("<p>Annulation</p>")
|
H.append("<p>Annulation</p>")
|
||||||
elif nf[0] == 1:
|
elif tf[0] == 1:
|
||||||
updiag = do_evaluation_upload_xls()
|
args = scu.get_request_args()
|
||||||
if updiag[0]:
|
evaluation = Evaluation.get_evaluation(args["evaluation_id"])
|
||||||
|
ok, diagnostic_msg = do_evaluations_upload_xls(
|
||||||
|
args["notefile"], evaluation=evaluation, comment=args["comment"]
|
||||||
|
)
|
||||||
|
if ok:
|
||||||
H.append(
|
H.append(
|
||||||
f"""
|
f"""
|
||||||
<div class="notes-chargees">
|
<div class="notes-chargees">
|
||||||
<div><b>Notes chargées !</b></div>
|
<div><b>Notes chargées !</b></div>
|
||||||
{updiag[1]}
|
{diagnostic_msg}
|
||||||
</div>
|
</div>
|
||||||
<a class="stdlink" href="{
|
<a class="stdlink" href="{
|
||||||
url_for("notes.moduleimpl_status",
|
url_for("notes.moduleimpl_status",
|
||||||
@ -898,7 +1042,7 @@ def saisie_notes_tableur(evaluation_id: int, group_ids=()):
|
|||||||
H.append(
|
H.append(
|
||||||
f"""
|
f"""
|
||||||
<p class="redboldtext">Notes non chargées !</p>
|
<p class="redboldtext">Notes non chargées !</p>
|
||||||
{updiag[1]}
|
{diagnostic_msg}
|
||||||
<p><a class="stdlink" href="{url_for("notes.saisie_notes_tableur",
|
<p><a class="stdlink" href="{url_for("notes.saisie_notes_tableur",
|
||||||
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id)
|
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id)
|
||||||
}">
|
}">
|
||||||
@ -955,7 +1099,8 @@ def saisie_notes_tableur(evaluation_id: int, group_ids=()):
|
|||||||
Remarques:
|
Remarques:
|
||||||
<ul>
|
<ul>
|
||||||
<li>le fichier Excel peut être incomplet: on peut ne saisir que quelques notes
|
<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;
|
et répéter l'opération plus tard (en téléchargeant un nouveau fichier ou en
|
||||||
|
passant par le formulaire de saisie);
|
||||||
</li>
|
</li>
|
||||||
<li>seules les valeurs des notes modifiées sont prises en compte;
|
<li>seules les valeurs des notes modifiées sont prises en compte;
|
||||||
</li>
|
</li>
|
||||||
@ -977,3 +1122,18 @@ def saisie_notes_tableur(evaluation_id: int, group_ids=()):
|
|||||||
)
|
)
|
||||||
H.append(html_sco_header.sco_footer())
|
H.append(html_sco_header.sco_footer())
|
||||||
return "\n".join(H)
|
return "\n".join(H)
|
||||||
|
|
||||||
|
|
||||||
|
def formsemestre_import_notes(formsemestre: FormSemestre, notefile, comment: str):
|
||||||
|
"""Importation de notes dans plusieurs évaluations du semestre"""
|
||||||
|
ok, diagnostic_msg = do_evaluations_upload_xls(
|
||||||
|
notefile, formsemestre=formsemestre, comment=comment
|
||||||
|
)
|
||||||
|
return render_template(
|
||||||
|
"formsemestre/import_notes_after.j2",
|
||||||
|
comment=comment,
|
||||||
|
ok=ok,
|
||||||
|
diagnostic_msg=diagnostic_msg,
|
||||||
|
sco=ScoData(formsemestre=formsemestre),
|
||||||
|
title="Importation des notes",
|
||||||
|
)
|
||||||
|
@ -156,6 +156,11 @@ def check_notes(
|
|||||||
|
|
||||||
for etudid, note in notes:
|
for etudid, note in notes:
|
||||||
if etudid not in etudids_inscrits_mod:
|
if etudid not in etudids_inscrits_mod:
|
||||||
|
# Si inscrit au formsemestre mais pas au module,
|
||||||
|
# accepte note "NI" uniquement (pour les imports excel multi-éval)
|
||||||
|
if (
|
||||||
|
etudid not in evaluation.moduleimpl.formsemestre.etudids_actifs()[0]
|
||||||
|
) or note != "NI":
|
||||||
etudids_non_inscrits.append(etudid)
|
etudids_non_inscrits.append(etudid)
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
@ -388,6 +393,27 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
|
|||||||
return html_sco_header.sco_header() + "\n".join(H) + html_sco_header.sco_footer()
|
return html_sco_header.sco_header() + "\n".join(H) + html_sco_header.sco_footer()
|
||||||
|
|
||||||
|
|
||||||
|
def _check_inscription(
|
||||||
|
etudid: int,
|
||||||
|
etudids_inscrits_sem: list[int],
|
||||||
|
etudids_inscrits_mod: set[int],
|
||||||
|
messages: list[str] | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""Vérifie inscription de etudid au moduleimpl et au semestre, et
|
||||||
|
- si étudiant non inscrit au semestre ou au module: lève NoteProcessError
|
||||||
|
"""
|
||||||
|
msg_err = ""
|
||||||
|
if etudid not in etudids_inscrits_sem:
|
||||||
|
msg_err = "non inscrit au semestre"
|
||||||
|
elif etudid not in etudids_inscrits_mod:
|
||||||
|
msg_err = "non inscrit au module"
|
||||||
|
if msg_err:
|
||||||
|
etud = Identite.query.get(etudid) if isinstance(etudid, int) else None
|
||||||
|
msg = f"étudiant {etud.nomprenom if etud else etudid} {msg_err}"
|
||||||
|
log(f"notes_add: {etudid} {msg}: aborting")
|
||||||
|
raise NoteProcessError(msg)
|
||||||
|
|
||||||
|
|
||||||
def notes_add(
|
def notes_add(
|
||||||
user: User,
|
user: User,
|
||||||
evaluation_id: int,
|
evaluation_id: int,
|
||||||
@ -424,22 +450,8 @@ def notes_add(
|
|||||||
for etudid, value in notes:
|
for etudid, value in notes:
|
||||||
|
|
||||||
if check_inscription:
|
if check_inscription:
|
||||||
msg_err, msg_warn = "", ""
|
_check_inscription(etudid, etudids_inscrits_sem, etudids_inscrits_mod)
|
||||||
if etudid not in etudids_inscrits_sem:
|
|
||||||
msg_err = "non inscrit au semestre"
|
|
||||||
elif etudid not in etudids_inscrits_mod:
|
|
||||||
msg_err = "non inscrit au module"
|
|
||||||
elif etudid not in etudids_actifs:
|
|
||||||
# DEM ou DEF
|
|
||||||
msg_warn = "démissionnaire ou défaillant (note enregistrée)"
|
|
||||||
if msg_err or msg_warn:
|
|
||||||
etud = Identite.query.get(etudid) if isinstance(etudid, int) else None
|
|
||||||
msg = f"étudiant {etud.nomprenom if etud else etudid} {msg_err or msg_warn}"
|
|
||||||
if msg_err:
|
|
||||||
log(f"notes_add: {etudid} non inscrit ou DEM/DEF: aborting")
|
|
||||||
raise NoteProcessError(msg)
|
|
||||||
if msg_warn:
|
|
||||||
messages.append(msg)
|
|
||||||
if (value is not None) and not isinstance(value, float):
|
if (value is not None) and not isinstance(value, float):
|
||||||
log(f"notes_add: {etudid} valeur de note invalide ({value}): aborting")
|
log(f"notes_add: {etudid} valeur de note invalide ({value}): aborting")
|
||||||
etud = Identite.query.get(etudid) if isinstance(etudid, int) else None
|
etud = Identite.query.get(etudid) if isinstance(etudid, int) else None
|
||||||
@ -470,14 +482,26 @@ def notes_add(
|
|||||||
date=now,
|
date=now,
|
||||||
do_it=do_it,
|
do_it=do_it,
|
||||||
)
|
)
|
||||||
|
|
||||||
if suppressed:
|
if suppressed:
|
||||||
nb_suppress += 1
|
nb_suppress += 1
|
||||||
|
|
||||||
if changed:
|
if changed:
|
||||||
etudids_changed.append(etudid)
|
etudids_changed.append(etudid)
|
||||||
|
# si change sur DEM/DEF ajoute message warning aux messages
|
||||||
|
if etudid not in etudids_actifs: # DEM ou DEF
|
||||||
|
etud = (
|
||||||
|
Identite.query.get(etudid) if isinstance(etudid, int) else None
|
||||||
|
)
|
||||||
|
messages.append(
|
||||||
|
f"""étudiant {etud.nomprenom if etud else etudid
|
||||||
|
} démissionnaire ou défaillant (note enregistrée)"""
|
||||||
|
)
|
||||||
|
|
||||||
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 NotImplementedError as exc: # XXX
|
|
||||||
|
except Exception as exc:
|
||||||
log("*** exception in notes_add")
|
log("*** exception in notes_add")
|
||||||
if do_it:
|
if do_it:
|
||||||
cnx.rollback() # abort
|
cnx.rollback() # abort
|
||||||
@ -485,10 +509,12 @@ def notes_add(
|
|||||||
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id)
|
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id)
|
||||||
sco_cache.EvaluationCache.delete(evaluation_id)
|
sco_cache.EvaluationCache.delete(evaluation_id)
|
||||||
raise ScoException from exc
|
raise ScoException from exc
|
||||||
|
|
||||||
if do_it:
|
if do_it:
|
||||||
cnx.commit()
|
cnx.commit()
|
||||||
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id)
|
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id)
|
||||||
sco_cache.EvaluationCache.delete(evaluation_id)
|
sco_cache.EvaluationCache.delete(evaluation_id)
|
||||||
|
|
||||||
return etudids_changed, nb_suppress, etudids_with_decision, messages
|
return etudids_changed, nb_suppress, etudids_with_decision, messages
|
||||||
|
|
||||||
|
|
||||||
|
@ -72,10 +72,23 @@ div.scobox.explanation {
|
|||||||
background-color: var(--sco-color-background);
|
background-color: var(--sco-color-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.scobox.success div.scobox-title {
|
||||||
|
color: white;
|
||||||
|
background-color: darkgreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.scobox.failure div.scobox-title {
|
||||||
|
color: white;
|
||||||
|
background-color: #c50000;
|
||||||
|
}
|
||||||
|
|
||||||
div.scobox div.scobox-title {
|
div.scobox div.scobox-title {
|
||||||
font-size: 120%;
|
font-size: 120%;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-top: 4px;
|
||||||
|
padding-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.scobox-buttons {
|
div.scobox-buttons {
|
||||||
|
@ -4,7 +4,9 @@
|
|||||||
{% block styles %}
|
{% block styles %}
|
||||||
{{super()}}
|
{{super()}}
|
||||||
<style>
|
<style>
|
||||||
|
div.vspace {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@ -35,7 +37,7 @@ Cette page permet d'importer des notes dans tout ou partie des évaluations du s
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="help" style="margin-top: 24px;">Une fois que le fichier tableur exporté ci-dessus est rempli, téléchargez-le
|
<div class="help vspace">Une fois que le fichier tableur exporté ci-dessus est rempli, téléchargez-le
|
||||||
ci-dessous.
|
ci-dessous.
|
||||||
Le texte "commentaire" sera associé à chaque note pour l'historique, il n'est jamais montré aux étudiants.
|
Le texte "commentaire" sera associé à chaque note pour l'historique, il n'est jamais montré aux étudiants.
|
||||||
</div>
|
</div>
|
||||||
@ -47,4 +49,32 @@ Le texte "commentaire" sera associé à chaque note pour l'historique, il n'est
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="help vspace">
|
||||||
|
À l'<b>étape 2</b>, indiquer le fichier Excel
|
||||||
|
<em>téléchargé à l'étape 1</em> et dans lequel on a saisi des notes.
|
||||||
|
<div class="vspace">
|
||||||
|
<b>Remarques :</b>
|
||||||
|
<ul>
|
||||||
|
<li>Le fichier Excel <em>doit impérativement être celui chargé à
|
||||||
|
l'étape 1 pour ce semestre</em>. Il n'est pas possible d'utiliser
|
||||||
|
une liste d'appel ou autre document Excel téléchargé d'une autre page.
|
||||||
|
</li>
|
||||||
|
<li>Ne pas supprimer les lignes et colonnes cachées, qui
|
||||||
|
contiennent des codes. Le fichier exporté contient toutles les
|
||||||
|
évaluations du semestre, vous pouvez au besoin supprimer certaines
|
||||||
|
colonnes d'évaluations.
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>Le fichier peut être incomplet: on
|
||||||
|
peut ne saisir que quelques notes et répéter l'opération plus tard (en
|
||||||
|
téléchargeant un nouveau fichier ou en passant par le formulaire de
|
||||||
|
saisie).
|
||||||
|
</li>
|
||||||
|
<li>Seules les valeurs des notes modifiées sont prises en
|
||||||
|
compte.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
54
app/templates/formsemestre/import_notes_after.j2
Normal file
54
app/templates/formsemestre/import_notes_after.j2
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
{% extends "sco_page.j2" %}
|
||||||
|
{% import 'wtf.j2' as wtf %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
{{super()}}
|
||||||
|
<style>
|
||||||
|
.import-diag ul.tf-msg {
|
||||||
|
padding-top: 8px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
.diag-change {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.diag-nochange {
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
<h2>Import de notes dans les évaluations du semestre</h2>
|
||||||
|
|
||||||
|
<div class="scobox {{ 'success' if ok else 'failure' }}">
|
||||||
|
<div class="scobox-title">
|
||||||
|
{% if ok %}
|
||||||
|
Notes importées avec succès
|
||||||
|
{% else %}
|
||||||
|
Erreur: aucune note chargée
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="import-diag">
|
||||||
|
{{ diagnostic_msg | safe }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="scobox">
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><a class="stdlink"
|
||||||
|
href="{{url_for('notes.formsemestre_recapcomplet',
|
||||||
|
scodoc_dept=g.scodoc_dept, formsemestre_id=sco.formsemestre.id,
|
||||||
|
tabformat='evals')}}">
|
||||||
|
Tableau de <em>toutes</em> les notes du semestre
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li><a class="stdlink"
|
||||||
|
href="{{url_for('notes.formsemestre_import_notes',
|
||||||
|
scodoc_dept=g.scodoc_dept, formsemestre_id=sco.formsemestre.id)}}">
|
||||||
|
Importer d'autres notes
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -1870,15 +1870,17 @@ def formsemestre_import_notes(formsemestre_id: int):
|
|||||||
# Handle file upload and form processing
|
# Handle file upload and form processing
|
||||||
notefile = form.notefile.data
|
notefile = form.notefile.data
|
||||||
comment = form.comment.data
|
comment = form.comment.data
|
||||||
# Save the file and process form data here
|
#
|
||||||
raise ScoValueError("unimplemented")
|
return sco_saisie_excel.formsemestre_import_notes(
|
||||||
return redirect(url_for("index"))
|
formsemestre, notefile, comment
|
||||||
|
)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"formsemestre/import_notes.j2",
|
"formsemestre/import_notes.j2",
|
||||||
evaluations=formsemestre.get_evaluations(),
|
evaluations=formsemestre.get_evaluations(),
|
||||||
form=form,
|
form=form,
|
||||||
formsemestre=formsemestre,
|
formsemestre=formsemestre,
|
||||||
|
title="Importation des notes",
|
||||||
sco=ScoData(formsemestre=formsemestre),
|
sco=ScoData(formsemestre=formsemestre),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# -*- mode: python -*-
|
# -*- mode: python -*-
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
SCOVERSION = "9.6.992"
|
SCOVERSION = "9.7.0"
|
||||||
|
|
||||||
SCONAME = "ScoDoc"
|
SCONAME = "ScoDoc"
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user