forked from ScoDoc/ScoDoc
Chargement notes excel: réorganisation du code
This commit is contained in:
parent
8f12c452df
commit
c4b44a1022
@ -197,18 +197,28 @@ class Identite(models.ScoDocModel):
|
||||
return cls.query.filter_by(**args).first_or_404()
|
||||
|
||||
@classmethod
|
||||
def get_etud(cls, etudid: int) -> "Identite":
|
||||
"""Etudiant ou 404, cherche uniquement dans le département courant"""
|
||||
def get_etud(cls, etudid: int, accept_none=False) -> "Identite":
|
||||
"""Etudiant ou 404 (ou None si accept_none),
|
||||
cherche uniquement dans le département courant.
|
||||
Si accept_none, return None si l'id est invalide ou ne correspond
|
||||
pas à un étudiant.
|
||||
"""
|
||||
if not isinstance(etudid, int):
|
||||
try:
|
||||
etudid = int(etudid)
|
||||
except (TypeError, ValueError):
|
||||
if accept_none:
|
||||
return None
|
||||
abort(404, "etudid invalide")
|
||||
if g.scodoc_dept:
|
||||
return cls.query.filter_by(
|
||||
id=etudid, dept_id=g.scodoc_dept_id
|
||||
).first_or_404()
|
||||
return cls.query.filter_by(id=etudid).first_or_404()
|
||||
|
||||
query = (
|
||||
cls.query.filter_by(id=etudid, dept_id=g.scodoc_dept_id)
|
||||
if g.scodoc_dept
|
||||
else cls.query.filter_by(id=etudid)
|
||||
)
|
||||
if accept_none:
|
||||
return query.first()
|
||||
return query.first_or_404()
|
||||
|
||||
@classmethod
|
||||
def create_etud(cls, **args) -> "Identite":
|
||||
|
@ -274,21 +274,39 @@ class ModuleImpl(ScoDocModel):
|
||||
return False
|
||||
return True
|
||||
|
||||
def est_inscrit(self, etud: Identite) -> bool:
|
||||
def est_inscrit(self, etud: Identite):
|
||||
"""
|
||||
Vérifie si l'étudiant est bien inscrit au moduleimpl (même si DEM ou DEF au semestre).
|
||||
(lent, pas de cache: pour un accès rapide, utiliser nt.modimpl_inscr_df).
|
||||
Retourne Vrai si inscrit au module, faux sinon.
|
||||
Retourne ModuleImplInscription si inscrit au module, False sinon.
|
||||
"""
|
||||
# vérifie inscrit au moduleimpl ET au formsemestre
|
||||
from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
||||
|
||||
is_module: int = (
|
||||
ModuleImplInscription.query.filter_by(
|
||||
etudid=etud.id, moduleimpl_id=self.id
|
||||
).count()
|
||||
> 0
|
||||
inscription = (
|
||||
ModuleImplInscription.query.filter_by(etudid=etud.id, moduleimpl_id=self.id)
|
||||
.join(ModuleImpl)
|
||||
.join(FormSemestre)
|
||||
.join(FormSemestreInscription)
|
||||
.filter_by(etudid=etud.id)
|
||||
.first()
|
||||
)
|
||||
|
||||
return is_module
|
||||
return inscription or False
|
||||
|
||||
def query_inscriptions(self) -> Query:
|
||||
"""Query ModuleImplInscription: inscrits au moduleimpl et au formsemestre
|
||||
(pas de cache: pour un accès rapide, utiliser nt.modimpl_inscr_df).
|
||||
"""
|
||||
from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
||||
|
||||
return (
|
||||
ModuleImplInscription.query.filter_by(moduleimpl_id=self.id)
|
||||
.join(ModuleImpl)
|
||||
.join(FormSemestre)
|
||||
.join(FormSemestreInscription)
|
||||
.filter_by(etudid=ModuleImplInscription.etudid)
|
||||
)
|
||||
|
||||
|
||||
# Enseignants (chargés de TD ou TP) d'un moduleimpl
|
||||
|
@ -1472,7 +1472,7 @@ def do_evaluation_listeetuds_groups(
|
||||
include_demdef: bool = False,
|
||||
) -> list[tuple[int, str]]:
|
||||
"""Donne la liste non triée des etudids inscrits à cette évaluation dans les
|
||||
groupes indiqués.
|
||||
groupes indiqués (donc inscrits au modimpl ET au formsemestre).
|
||||
Si getallstudents==True, donne tous les étudiants inscrits à cette
|
||||
evaluation.
|
||||
Si include_demdef, compte aussi les etudiants démissionnaires et défaillants
|
||||
|
@ -24,11 +24,11 @@
|
||||
|
||||
"""Fichier excel de saisie des notes
|
||||
"""
|
||||
|
||||
from collections import defaultdict
|
||||
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 import flash, g, request, url_for
|
||||
from flask_login import current_user
|
||||
|
||||
from app.models import Evaluation, Identite, Module, ScolarNews
|
||||
@ -49,9 +49,7 @@ from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||
FONT_NAME = "Arial"
|
||||
|
||||
|
||||
def excel_feuille_saisie(
|
||||
evaluation: "Evaluation", titreannee, description, rows: list[dict]
|
||||
):
|
||||
def excel_feuille_saisie(evaluation: "Evaluation", rows: list[dict]):
|
||||
"""Genere feuille excel pour saisie des notes.
|
||||
E: evaluation (dict)
|
||||
lines: liste de tuples
|
||||
@ -59,106 +57,15 @@ def excel_feuille_saisie(
|
||||
"""
|
||||
sheet_name = "Saisie notes"
|
||||
ws = ScoExcelSheet(sheet_name)
|
||||
styles = _build_styles()
|
||||
nb_lines_titles = _insert_top_title(ws, styles, evaluation=evaluation)
|
||||
|
||||
# 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"]),
|
||||
]
|
||||
_insert_line_titles(
|
||||
ws,
|
||||
nb_lines_titles,
|
||||
nb_rows_in_table=len(rows),
|
||||
evaluations=[evaluation],
|
||||
styles=styles,
|
||||
)
|
||||
|
||||
# etudiants
|
||||
@ -209,6 +116,151 @@ def excel_feuille_saisie(
|
||||
)
|
||||
|
||||
|
||||
def _insert_line_titles(
|
||||
ws,
|
||||
current_line,
|
||||
nb_rows_in_table: int = 0,
|
||||
evaluations: list[Evaluation] = None,
|
||||
styles: dict = None,
|
||||
) -> int:
|
||||
"""Ligne(s) des titres, avec filtre auto excel.
|
||||
current_line : nb de lignes déjà dans le tableau
|
||||
nb_rows_in_table: nombre de ligne dans tableau à trier pour le filtre (nb d'étudiants)
|
||||
Renvoie nombre de lignes ajoutées (si plusieurs évaluations, indique les eval
|
||||
ids au dessus des titres)
|
||||
"""
|
||||
# WIP
|
||||
assert len(evaluations) == 1
|
||||
evaluation = evaluations[0]
|
||||
|
||||
# Filtre auto excel sur colonnes
|
||||
filter_top = current_line + 1
|
||||
filter_bottom = current_line + 1 + nb_rows_in_table
|
||||
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}")
|
||||
|
||||
# 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"]),
|
||||
]
|
||||
)
|
||||
return 1 # WIP
|
||||
|
||||
|
||||
def _build_styles() -> dict:
|
||||
"""Déclare le styles excel"""
|
||||
|
||||
# bordures
|
||||
side_thin = Side(border_style="thin", color=COLORS.BLACK.value)
|
||||
border_top = Border(top=side_thin)
|
||||
|
||||
# fonds
|
||||
fill_light_yellow = PatternFill(
|
||||
patternType="solid", fgColor=COLORS.LIGHT_YELLOW.value
|
||||
)
|
||||
|
||||
# styles
|
||||
font_base = Font(name=FONT_NAME, size=12)
|
||||
return {
|
||||
"base": {"font": font_base},
|
||||
"titres": {"font": Font(name=FONT_NAME, bold=True, size=14)},
|
||||
"explanation": {
|
||||
"font": Font(name=FONT_NAME, size=12, italic=True, color=COLORS.RED.value)
|
||||
},
|
||||
"read-only": { # cells read-only
|
||||
"font": Font(name=FONT_NAME, color=COLORS.PURPLE.value),
|
||||
"border": Border(right=side_thin),
|
||||
},
|
||||
"dem": {
|
||||
"font": Font(name=FONT_NAME, color=COLORS.BROWN.value),
|
||||
"border": border_top,
|
||||
},
|
||||
"nom": { # style pour nom, prenom, groupe
|
||||
"font": font_base,
|
||||
"border": border_top,
|
||||
},
|
||||
"notes": {
|
||||
"alignment": Alignment(horizontal="right"),
|
||||
"font": Font(name=FONT_NAME, bold=True),
|
||||
"number_format": FORMAT_GENERAL,
|
||||
"fill": fill_light_yellow,
|
||||
"border": border_top,
|
||||
},
|
||||
"comment": {
|
||||
"font": Font(name=FONT_NAME, size=9, color=COLORS.BLUE.value),
|
||||
"border": border_top,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _insert_top_title(
|
||||
ws, styles: dict, evaluation: Evaluation = None, description=""
|
||||
) -> int:
|
||||
"""Insère les lignes de titre de la feuille (suivies d'une ligne blanche)
|
||||
renvoie le nb de lignes insérées
|
||||
"""
|
||||
n = 0
|
||||
# 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=[""],
|
||||
)
|
||||
n += 1
|
||||
# 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=[""]
|
||||
)
|
||||
n += 2
|
||||
# Nom du semestre
|
||||
titre_annee = evaluation.moduleimpl.formsemestre.titre_annee()
|
||||
ws.append_single_cell_row(
|
||||
scu.unescape_html(titre_annee), styles["titres"], prefix=[""]
|
||||
)
|
||||
n += 1
|
||||
# description evaluation
|
||||
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}"""
|
||||
|
||||
mod_responsable = sco_users.user_info(evaluation.moduleimpl.responsable_id)
|
||||
description = f"""{eval_titre} en {evaluation.moduleimpl.module.abbrev or ""} ({
|
||||
evaluation.moduleimpl.module.code
|
||||
}) resp. {mod_responsable["prenomnom"]}"""
|
||||
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=[""],
|
||||
)
|
||||
n += 2
|
||||
# ligne blanche
|
||||
ws.append_blank_row()
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
def _insert_bottom_help(ws, styles: dict):
|
||||
ws.append_row([None, ws.make_cell("Code notes", styles["titres"])])
|
||||
ws.append_row(
|
||||
@ -254,24 +306,11 @@ def feuille_saisie_notes(evaluation_id, group_ids: list[int] = None):
|
||||
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,
|
||||
@ -315,99 +354,110 @@ def feuille_saisie_notes(evaluation_id, group_ids: list[int] = None):
|
||||
}
|
||||
)
|
||||
|
||||
eval_name = f"{evaluation.moduleimpl.module.code}-{indication_date}"
|
||||
filename = f"notes_{eval_name}_{gr_title_filename}"
|
||||
xls = excel_feuille_saisie(
|
||||
evaluation, formsemestre.titre_annee(), description, rows=rows
|
||||
)
|
||||
xls = excel_feuille_saisie(evaluation, rows=rows)
|
||||
return scu.send_file(xls, filename, scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE)
|
||||
|
||||
|
||||
def do_evaluation_upload_xls():
|
||||
def do_evaluation_upload_xls() -> tuple[bool, str]:
|
||||
"""
|
||||
Soumission d'un fichier XLS (evaluation_id, notefile)
|
||||
return:
|
||||
ok: bool
|
||||
msg: message diagonistic à affciher
|
||||
"""
|
||||
args = scu.get_request_args()
|
||||
evaluation_id = int(args["evaluation_id"])
|
||||
comment = args["comment"]
|
||||
evaluation = Evaluation.get_evaluation(evaluation_id)
|
||||
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, lines = sco_excel.excel_file_to_list(args["notefile"])
|
||||
diag, rows = 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)")
|
||||
if not rows:
|
||||
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()
|
||||
row_title_idx, evaluations, evaluations_col_idx = _get_sheet_evaluations(
|
||||
rows, evaluation=evaluation, diag=diag
|
||||
)
|
||||
# --- 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()
|
||||
notes_by_eval = defaultdict(
|
||||
list
|
||||
) # { evaluation_id : [ (etudid, note_value), ... ] }
|
||||
ni = row_title_idx + 1
|
||||
for row in rows[row_title_idx + 1 :]:
|
||||
if row:
|
||||
cell0 = row[0].strip()
|
||||
if cell0 and cell0[0] == "!":
|
||||
etudid = cell0[1:]
|
||||
# check etud
|
||||
etud = Identite.get_etud(etudid, accept_none=True)
|
||||
if not etud:
|
||||
diag.append(
|
||||
f"étudiant id invalide en ligne {ni+1}"
|
||||
) # ligne excel à partir de 1
|
||||
else:
|
||||
_read_notes_evaluations(
|
||||
row,
|
||||
etud,
|
||||
evaluations,
|
||||
notes_by_eval,
|
||||
evaluations_col_idx,
|
||||
)
|
||||
ni += 1
|
||||
|
||||
etudids_changed, nb_suppress, etudids_with_decisions, messages = (
|
||||
sco_saisie_notes.notes_add(
|
||||
current_user, evaluation_id, valid_notes, comment
|
||||
)
|
||||
# -- Check values de chaque évaluation
|
||||
valid_notes_by_eval, etudids_without_notes_by_eval, etudids_absents_by_eval = (
|
||||
_check_notes_evaluations(evaluations, notes_by_eval, diag)
|
||||
)
|
||||
# news
|
||||
|
||||
# -- Enregistre les notes de chaque évaluation
|
||||
messages_by_eval: dict[int, str] = {}
|
||||
etudids_with_decisions = set()
|
||||
for evaluation in evaluations:
|
||||
valid_notes = valid_notes_by_eval.get(evaluation.id)
|
||||
if not valid_notes:
|
||||
continue
|
||||
etudids_changed, nb_suppress, etudids_with_decisions_eval, messages = (
|
||||
sco_saisie_notes.notes_add(
|
||||
current_user,
|
||||
evaluation.id,
|
||||
valid_notes_by_eval[evaluation.id],
|
||||
comment,
|
||||
)
|
||||
)
|
||||
etudids_with_decisions |= set(etudids_with_decisions_eval)
|
||||
msg = f"""<div class="diag-evaluation">
|
||||
<ul>
|
||||
<li><div>Module {evaluation.moduleimpl.module.code} :
|
||||
évaluation {evaluation.description} {evaluation.descr_date()}
|
||||
</div>
|
||||
<div>
|
||||
{len(etudids_changed)} notes changées
|
||||
({len(etudids_without_notes_by_eval[evaluation.id])} sans notes,
|
||||
{len(etudids_absents_by_eval[evaluation.id])} absents,
|
||||
{nb_suppress} note supprimées)
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
"""
|
||||
if messages:
|
||||
msg += f"""<div class="warning">Attention :
|
||||
<ul>
|
||||
<li>{
|
||||
'</li><li>'.join(messages)
|
||||
}
|
||||
</li>
|
||||
</ul>
|
||||
</div>"""
|
||||
msg += """</div>"""
|
||||
messages_by_eval[evaluation.id] = msg
|
||||
|
||||
# -- News
|
||||
module: Module = evaluation.moduleimpl.module
|
||||
status_url = url_for(
|
||||
"notes.moduleimpl_status",
|
||||
@ -424,24 +474,11 @@ def do_evaluation_upload_xls():
|
||||
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>
|
||||
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 1, msg
|
||||
return True, msg
|
||||
|
||||
except InvalidNoteValue:
|
||||
if diag:
|
||||
@ -452,10 +489,119 @@ def do_evaluation_upload_xls():
|
||||
)
|
||||
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>"
|
||||
return False, msg + "<p>(pas de notes modifiées)</p>"
|
||||
|
||||
|
||||
def saisie_notes_tableur(evaluation_id, group_ids=()):
|
||||
def _check_notes_evaluations(
|
||||
evaluations: list[Evaluation],
|
||||
notes_by_eval: dict[int, list[tuple[int, str]]],
|
||||
diag: list[str],
|
||||
) -> tuple[dict[int, list[tuple[int, str]]], list[int], list[int]]:
|
||||
"""Vérifie que les notes pour ces évaluations sont valides.
|
||||
Raise InvalidNoteValue et rempli diag si ce n'est pas le cas.
|
||||
Renvoie un dict donnant la liste des notes converties pour chaque évaluation.
|
||||
"""
|
||||
valid_notes_by_eval = {}
|
||||
etudids_without_notes_by_eval = {}
|
||||
etudids_absents_by_eval = {}
|
||||
for evaluation in evaluations:
|
||||
(
|
||||
valid_notes_by_eval[evaluation.id],
|
||||
invalids,
|
||||
etudids_without_notes_by_eval[evaluation.id],
|
||||
etudids_absents_by_eval[evaluation.id],
|
||||
etudids_non_inscrits,
|
||||
) = sco_saisie_notes.check_notes(notes_by_eval[evaluation.id], 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))
|
||||
else:
|
||||
diag.append("Notes invalides pour plus de 25 étudiants")
|
||||
raise InvalidNoteValue()
|
||||
if etudids_non_inscrits:
|
||||
diag.append(
|
||||
f"""Erreur: la feuille contient {len(etudids_non_inscrits)
|
||||
} étudiants non inscrits</p>"""
|
||||
)
|
||||
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()
|
||||
return valid_notes_by_eval, etudids_without_notes_by_eval, etudids_absents_by_eval
|
||||
|
||||
|
||||
def _read_notes_evaluations(
|
||||
row: list[str],
|
||||
etud: Identite,
|
||||
evaluations: list[Evaluation],
|
||||
notes_by_eval: dict[int, list[tuple[int, str]]],
|
||||
evaluations_col_idx: dict[int, int],
|
||||
):
|
||||
"""Lit les notes sur une ligne (étudiant etud).
|
||||
Ne vérifie pas la valeur de la note.
|
||||
"""
|
||||
for evaluation in evaluations:
|
||||
col_idx = evaluations_col_idx[evaluation.id]
|
||||
if len(row) > col_idx:
|
||||
val = row[col_idx].strip()
|
||||
else:
|
||||
val = "" # ligne courte: cellule vide
|
||||
notes_by_eval[evaluation.id].append((etud.id, val))
|
||||
|
||||
|
||||
def _get_sheet_evaluations(
|
||||
rows: list[list[str]], evaluation: Evaluation | None = None, diag: list[str] = None
|
||||
) -> tuple[int, list[Evaluation], dict[int, int]]:
|
||||
"""
|
||||
rows: les valeurs (str) des cellules de la feuille
|
||||
diag: liste dans laquelle accumuler les messages d'erreur
|
||||
evaluation (optionnel): l'évaluation que l'on cherche à remplir (pour feuille mono-évaluation)
|
||||
formsemestre (optionnel): le formsemestre dans lequel sont les évaluations à remplir
|
||||
formsemestre ou evaluation doivent être indiqués.
|
||||
|
||||
Résultat:
|
||||
row_title_idx: l'indice (à partir de 0) de la ligne titre (après laquelle commencent les notes)
|
||||
evaluations: liste des évaluations à remplir
|
||||
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:
|
||||
sheet_eval_id = int(eval_code[1:])
|
||||
except ValueError:
|
||||
sheet_eval_id = None
|
||||
if sheet_eval_id != evaluation.id:
|
||||
diag.append(
|
||||
f"""Erreur: fichier invalide: le code d'évaluation de correspond pas ! ('{
|
||||
sheet_eval_id or ('non trouvé')}' != '{evaluation.id}')"""
|
||||
)
|
||||
raise InvalidNoteValue()
|
||||
|
||||
return i, [evaluation], {evaluation.id: 4}
|
||||
|
||||
|
||||
def saisie_notes_tableur(evaluation_id: int, group_ids=()):
|
||||
"""Saisie des notes via un fichier Excel"""
|
||||
evaluation = Evaluation.query.get_or_404(evaluation_id)
|
||||
moduleimpl_id = evaluation.moduleimpl.id
|
||||
@ -562,9 +708,11 @@ def saisie_notes_tableur(evaluation_id, group_ids=()):
|
||||
elif nf[0] == 1:
|
||||
updiag = do_evaluation_upload_xls()
|
||||
if updiag[0]:
|
||||
H.append(updiag[1])
|
||||
H.append(
|
||||
f"""<p>Notes chargées.
|
||||
f"""
|
||||
<div><b>Notes chargées.</b><div>
|
||||
{updiag[1]}
|
||||
<div>
|
||||
<a class="stdlink" href="{
|
||||
url_for("notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id)
|
||||
@ -578,7 +726,7 @@ def saisie_notes_tableur(evaluation_id, group_ids=()):
|
||||
<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>"""
|
||||
</div>"""
|
||||
)
|
||||
else:
|
||||
H.append(
|
||||
|
@ -76,15 +76,17 @@ from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
def convert_note_from_string(
|
||||
note: str,
|
||||
note_max,
|
||||
note_min=scu.NOTES_MIN,
|
||||
note_max: float,
|
||||
note_min: float = scu.NOTES_MIN,
|
||||
etudid: int = None,
|
||||
absents: list[int] = None,
|
||||
tosuppress: list[int] = None,
|
||||
invalids: list[int] = None,
|
||||
):
|
||||
) -> tuple[float, bool]:
|
||||
"""converti une valeur (chaine saisie) vers une note numérique (float)
|
||||
Les listes absents, tosuppress et invalids sont modifiées
|
||||
Les listes absents et invalids sont modifiées.
|
||||
Return:
|
||||
note_value: float (valeur de la note ou code EXC, ATT, ...)
|
||||
invalid: True si note invalide (eg hors barème)
|
||||
"""
|
||||
invalid = False
|
||||
note_value = None
|
||||
@ -98,7 +100,6 @@ def convert_note_from_string(
|
||||
note_value = scu.NOTES_ATTENTE
|
||||
elif note[:3] == "SUP":
|
||||
note_value = scu.NOTES_SUPPRESS
|
||||
tosuppress.append(etudid)
|
||||
else:
|
||||
try:
|
||||
note_value = float(note)
|
||||
@ -111,12 +112,22 @@ def convert_note_from_string(
|
||||
return note_value, invalid
|
||||
|
||||
|
||||
def check_notes(notes: list[(int, float | str)], evaluation: Evaluation):
|
||||
"""notes is a list of tuples (etudid, value)
|
||||
mod is the module (used to ckeck type, for malus)
|
||||
returns list of valid notes (etudid, float value)
|
||||
def check_notes(
|
||||
notes: list[(int, float | str)], evaluation: Evaluation
|
||||
) -> tuple[list[tuple[int, float]], list[int], list[int], list[int], list[int]]:
|
||||
"""Vérifie et converti les valeurs des notes pour une évaluation.
|
||||
|
||||
notes: list of tuples (etudid, value)
|
||||
evaluation: target
|
||||
|
||||
Returns
|
||||
valid_notes: list of valid notes (etudid, float value)
|
||||
and 4 lists of etudid:
|
||||
etudids_invalids, etudids_without_notes, etudids_absents, etudid_to_suppress
|
||||
etudids_invalids : etudid avec notes invalides
|
||||
etudids_without_notes: etudid sans notes (champs vides)
|
||||
etudids_absents : etudid avec note ABS
|
||||
etudids_non_inscrits : etudid non inscrits à ce module
|
||||
(ne considère pas l'inscr. au semestre)
|
||||
"""
|
||||
note_max = evaluation.note_max or 0.0
|
||||
module: Module = evaluation.moduleimpl.module
|
||||
@ -133,18 +144,25 @@ def check_notes(notes: list[(int, float | str)], evaluation: Evaluation):
|
||||
note_min = -20.0
|
||||
else:
|
||||
raise ValueError("Invalid module type") # bug
|
||||
valid_notes = [] # liste (etudid, note) des notes ok (ou absent)
|
||||
etudids_invalids = [] # etudid avec notes invalides
|
||||
etudids_without_notes = [] # etudid sans notes (champs vides)
|
||||
etudids_absents = [] # etudid absents
|
||||
etudid_to_suppress = [] # etudids avec ancienne note à supprimer
|
||||
# Vérifie inscription au module (même DEM/DEF)
|
||||
etudids_inscrits_mod = {
|
||||
i.etudid for i in evaluation.moduleimpl.query_inscriptions()
|
||||
}
|
||||
valid_notes = []
|
||||
etudids_invalids = []
|
||||
etudids_without_notes = []
|
||||
etudids_absents = []
|
||||
etudids_non_inscrits = []
|
||||
|
||||
for etudid, note in notes:
|
||||
note = str(note).strip().upper()
|
||||
if etudid not in etudids_inscrits_mod:
|
||||
etudids_non_inscrits.append(etudid)
|
||||
continue
|
||||
try:
|
||||
etudid = int(etudid) #
|
||||
except ValueError as exc:
|
||||
raise ScoValueError(f"Code étudiant ({etudid}) invalide") from exc
|
||||
note = str(note).strip().upper()
|
||||
if note[:3] == "DEM":
|
||||
continue # skip !
|
||||
if note:
|
||||
@ -154,7 +172,6 @@ def check_notes(notes: list[(int, float | str)], evaluation: Evaluation):
|
||||
note_min=note_min,
|
||||
etudid=etudid,
|
||||
absents=etudids_absents,
|
||||
tosuppress=etudid_to_suppress,
|
||||
invalids=etudids_invalids,
|
||||
)
|
||||
if not invalid:
|
||||
@ -166,7 +183,7 @@ def check_notes(notes: list[(int, float | str)], evaluation: Evaluation):
|
||||
etudids_invalids,
|
||||
etudids_without_notes,
|
||||
etudids_absents,
|
||||
etudid_to_suppress,
|
||||
etudids_non_inscrits,
|
||||
)
|
||||
|
||||
|
||||
@ -387,6 +404,8 @@ def notes_add(
|
||||
Nota:
|
||||
- si la note existe deja avec valeur distincte, ajoute une entree au log (notes_notes_log)
|
||||
|
||||
Raise NoteProcessError si note invalide ou étudiant non inscrit.
|
||||
|
||||
Return: tuple (etudids_changed, nb_suppress, etudids_with_decision, messages)
|
||||
|
||||
messages = list de messages d'avertissement/information pour l'utilisateur
|
||||
@ -396,10 +415,7 @@ def notes_add(
|
||||
messages = []
|
||||
# Vérifie inscription au module (même DEM/DEF)
|
||||
etudids_inscrits_mod = {
|
||||
x[0]
|
||||
for x in sco_groups.do_evaluation_listeetuds_groups(
|
||||
evaluation_id, getallstudents=True, include_demdef=True
|
||||
)
|
||||
i.etudid for i in evaluation.moduleimpl.query_inscriptions()
|
||||
}
|
||||
# Les étudiants inscrits au semestre et ceux "actifs" (ni DEM ni DEF)
|
||||
etudids_inscrits_sem, etudids_actifs = (
|
||||
@ -759,13 +775,17 @@ def get_sorted_etuds_notes(
|
||||
e["val"] = scu.fmt_note(
|
||||
notes_db[etudid]["value"], fixed_precision_str=False
|
||||
)
|
||||
comment = notes_db[etudid]["comment"]
|
||||
if comment is None:
|
||||
comment = ""
|
||||
e["explanation"] = "%s (%s) %s" % (
|
||||
notes_db[etudid]["date"].strftime("%d/%m/%y %Hh%M"),
|
||||
notes_db[etudid]["uid"],
|
||||
comment,
|
||||
user = (
|
||||
User.query.get(notes_db[etudid]["uid"])
|
||||
if notes_db[etudid]["uid"]
|
||||
else None
|
||||
)
|
||||
e["explanation"] = (
|
||||
f"""{
|
||||
notes_db[etudid]["date"].strftime("%d/%m/%y %Hh%M")
|
||||
} par {user.get_nomplogin() if user else '?'
|
||||
} {(' : ' + notes_db[etudid]["comment"]) if notes_db[etudid]["comment"] else ''}
|
||||
"""
|
||||
)
|
||||
else:
|
||||
e["val"] = ""
|
||||
|
@ -355,7 +355,7 @@ def external_ue_create_form(formsemestre_id: int, etudid: int):
|
||||
else:
|
||||
note = tf[2]["note"].strip().upper()
|
||||
note_value, invalid = sco_saisie_notes.convert_note_from_string(
|
||||
note, 20.0, etudid=etudid, absents=[], tosuppress=[], invalids=[]
|
||||
note, 20.0, etudid=etudid, absents=[], invalids=[]
|
||||
)
|
||||
if invalid:
|
||||
return (
|
||||
|
@ -46,7 +46,7 @@ Opérations:
|
||||
"""
|
||||
|
||||
import datetime
|
||||
from flask import request
|
||||
from flask import g, request, url_for
|
||||
|
||||
from app.models import Evaluation, FormSemestre
|
||||
from app.scodoc.intervals import intervalmap
|
||||
@ -164,9 +164,12 @@ def evaluation_list_operations(evaluation_id: int):
|
||||
columns_ids=columns_ids,
|
||||
rows=operations,
|
||||
html_sortable=False,
|
||||
html_title=f"""<h2>Opérations sur l'évaluation {evaluation.description} {
|
||||
evaluation.date_debut.strftime("du %d/%m/%Y") if evaluation.date_debut else "(sans date)"
|
||||
}</h2>""",
|
||||
html_title=f"""<h2>Opérations sur l'évaluation
|
||||
<a class="stdlink" href="{
|
||||
url_for("notes.evaluation_listenotes", scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id)
|
||||
}">{evaluation.description}</a>
|
||||
{evaluation.date_debut.strftime("du %d/%m/%Y") if evaluation.date_debut else "(sans date)"}
|
||||
</h2>""",
|
||||
preferences=sco_preferences.SemPreferences(
|
||||
evaluation.moduleimpl.formsemestre_id
|
||||
),
|
||||
|
@ -4908,4 +4908,9 @@ div.cas_etat_certif_ssl {
|
||||
margin-bottom: 8px;
|
||||
font-style: italic;
|
||||
color: rgb(231, 0, 0);
|
||||
}
|
||||
|
||||
|
||||
.diag-evaluation {
|
||||
color: green;
|
||||
}
|
@ -204,7 +204,6 @@ def note_les_modules(doc: dict, formsemestre_titre: str = ""):
|
||||
note_min=scu.NOTES_MIN,
|
||||
etudid=etud.id,
|
||||
absents=[],
|
||||
tosuppress=[],
|
||||
invalids=[],
|
||||
)
|
||||
assert not invalid # valeur note invalide
|
||||
|
Loading…
Reference in New Issue
Block a user