2024-06-28 19:03:20 +02:00
|
|
|
##############################################################################
|
|
|
|
#
|
|
|
|
# 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
|
|
|
|
"""
|
2024-06-30 23:00:42 +02:00
|
|
|
from collections import defaultdict
|
2024-06-28 19:03:20 +02:00
|
|
|
from openpyxl.styles import Font, Border, Side, Alignment, PatternFill
|
|
|
|
from openpyxl.styles.numbers import FORMAT_GENERAL
|
|
|
|
|
2024-06-30 23:00:42 +02:00
|
|
|
from flask import flash, g, request, url_for
|
2024-06-28 19:03:20 +02:00
|
|
|
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"
|
|
|
|
|
|
|
|
|
2024-06-30 23:00:42 +02:00
|
|
|
def excel_feuille_saisie(evaluation: "Evaluation", rows: list[dict]):
|
2024-06-28 19:03:20 +02:00
|
|
|
"""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)
|
2024-06-30 23:00:42 +02:00
|
|
|
styles = _build_styles()
|
|
|
|
nb_lines_titles = _insert_top_title(ws, styles, evaluation=evaluation)
|
|
|
|
|
|
|
|
_insert_line_titles(
|
|
|
|
ws,
|
|
|
|
nb_lines_titles,
|
|
|
|
nb_rows_in_table=len(rows),
|
|
|
|
evaluations=[evaluation],
|
|
|
|
styles=styles,
|
|
|
|
)
|
2024-06-28 19:03:20 +02:00
|
|
|
|
2024-06-30 23:00:42 +02:00
|
|
|
# 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_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"""
|
2024-06-28 19:03:20 +02:00
|
|
|
|
|
|
|
# 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
|
2024-06-30 23:00:42 +02:00
|
|
|
font_base = Font(name=FONT_NAME, size=12)
|
|
|
|
return {
|
2024-06-28 19:03:20 +02:00
|
|
|
"base": {"font": font_base},
|
2024-06-30 23:00:42 +02:00
|
|
|
"titres": {"font": Font(name=FONT_NAME, bold=True, size=14)},
|
|
|
|
"explanation": {
|
|
|
|
"font": Font(name=FONT_NAME, size=12, italic=True, color=COLORS.RED.value)
|
|
|
|
},
|
2024-06-28 19:03:20 +02:00
|
|
|
"read-only": { # cells read-only
|
2024-06-30 23:00:42 +02:00
|
|
|
"font": Font(name=FONT_NAME, color=COLORS.PURPLE.value),
|
|
|
|
"border": Border(right=side_thin),
|
2024-06-28 19:03:20 +02:00
|
|
|
},
|
|
|
|
"dem": {
|
2024-06-30 23:00:42 +02:00
|
|
|
"font": Font(name=FONT_NAME, color=COLORS.BROWN.value),
|
2024-06-28 19:03:20 +02:00
|
|
|
"border": border_top,
|
|
|
|
},
|
|
|
|
"nom": { # style pour nom, prenom, groupe
|
|
|
|
"font": font_base,
|
|
|
|
"border": border_top,
|
|
|
|
},
|
|
|
|
"notes": {
|
|
|
|
"alignment": Alignment(horizontal="right"),
|
2024-06-30 23:00:42 +02:00
|
|
|
"font": Font(name=FONT_NAME, bold=True),
|
2024-06-28 19:03:20 +02:00
|
|
|
"number_format": FORMAT_GENERAL,
|
|
|
|
"fill": fill_light_yellow,
|
|
|
|
"border": border_top,
|
|
|
|
},
|
|
|
|
"comment": {
|
2024-06-30 23:00:42 +02:00
|
|
|
"font": Font(name=FONT_NAME, size=9, color=COLORS.BLUE.value),
|
2024-06-28 19:03:20 +02:00
|
|
|
"border": border_top,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2024-06-30 23:00:42 +02:00
|
|
|
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
|
2024-06-28 19:03:20 +02:00
|
|
|
# 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=[""],
|
|
|
|
)
|
2024-06-30 23:00:42 +02:00
|
|
|
n += 1
|
2024-06-28 19:03:20 +02:00
|
|
|
# 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=[""]
|
|
|
|
)
|
2024-06-30 23:00:42 +02:00
|
|
|
n += 2
|
2024-06-28 19:03:20 +02:00
|
|
|
# Nom du semestre
|
2024-06-30 23:00:42 +02:00
|
|
|
titre_annee = evaluation.moduleimpl.formsemestre.titre_annee()
|
2024-06-28 19:03:20 +02:00
|
|
|
ws.append_single_cell_row(
|
2024-06-30 23:00:42 +02:00
|
|
|
scu.unescape_html(titre_annee), styles["titres"], prefix=[""]
|
2024-06-28 19:03:20 +02:00
|
|
|
)
|
2024-06-30 23:00:42 +02:00
|
|
|
n += 1
|
2024-06-28 19:03:20 +02:00
|
|
|
# description evaluation
|
2024-06-30 23:00:42 +02:00
|
|
|
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"]}"""
|
2024-06-28 19:03:20 +02:00
|
|
|
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=[""],
|
|
|
|
)
|
2024-06-30 23:00:42 +02:00
|
|
|
n += 2
|
2024-06-28 19:03:20 +02:00
|
|
|
# ligne blanche
|
|
|
|
ws.append_blank_row()
|
2024-06-30 23:00:42 +02:00
|
|
|
n += 1
|
|
|
|
return n
|
2024-06-28 19:03:20 +02:00
|
|
|
|
|
|
|
|
|
|
|
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
|
2024-06-30 23:00:42 +02:00
|
|
|
|
2024-06-28 19:03:20 +02:00
|
|
|
if evaluation.date_debut:
|
|
|
|
indication_date = evaluation.date_debut.date().isoformat()
|
|
|
|
else:
|
|
|
|
indication_date = scu.sanitize_filename(evaluation.description)[:12]
|
|
|
|
|
|
|
|
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"],
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2024-06-30 23:00:42 +02:00
|
|
|
eval_name = f"{evaluation.moduleimpl.module.code}-{indication_date}"
|
2024-06-28 19:03:20 +02:00
|
|
|
filename = f"notes_{eval_name}_{gr_title_filename}"
|
2024-06-30 23:00:42 +02:00
|
|
|
xls = excel_feuille_saisie(evaluation, rows=rows)
|
2024-06-28 19:03:20 +02:00
|
|
|
return scu.send_file(xls, filename, scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE)
|
|
|
|
|
|
|
|
|
2024-06-30 23:00:42 +02:00
|
|
|
def do_evaluation_upload_xls() -> tuple[bool, str]:
|
2024-06-28 19:03:20 +02:00
|
|
|
"""
|
|
|
|
Soumission d'un fichier XLS (evaluation_id, notefile)
|
2024-06-30 23:00:42 +02:00
|
|
|
return:
|
|
|
|
ok: bool
|
|
|
|
msg: message diagonistic à affciher
|
2024-06-28 19:03:20 +02:00
|
|
|
"""
|
|
|
|
args = scu.get_request_args()
|
|
|
|
comment = args["comment"]
|
2024-06-30 23:00:42 +02:00
|
|
|
evaluation = Evaluation.get_evaluation(args["evaluation_id"])
|
2024-06-28 19:03:20 +02:00
|
|
|
|
|
|
|
# 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}")
|
|
|
|
#
|
2024-06-30 23:00:42 +02:00
|
|
|
diag, rows = sco_excel.excel_file_to_list(args["notefile"])
|
2024-06-28 19:03:20 +02:00
|
|
|
try:
|
2024-06-30 23:00:42 +02:00
|
|
|
if not rows:
|
2024-06-28 19:03:20 +02:00
|
|
|
raise InvalidNoteValue()
|
|
|
|
|
2024-06-30 23:00:42 +02:00
|
|
|
row_title_idx, evaluations, evaluations_col_idx = _get_sheet_evaluations(
|
|
|
|
rows, evaluation=evaluation, diag=diag
|
|
|
|
)
|
2024-06-28 19:03:20 +02:00
|
|
|
# --- get notes -> list (etudid, value)
|
|
|
|
# ignore toutes les lignes ne commençant pas par !
|
2024-06-30 23:00:42 +02:00
|
|
|
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
|
|
|
|
|
|
|
|
# -- 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)
|
2024-06-28 19:03:20 +02:00
|
|
|
)
|
|
|
|
|
2024-06-30 23:00:42 +02:00
|
|
|
# -- 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,
|
|
|
|
)
|
2024-06-28 19:03:20 +02:00
|
|
|
)
|
2024-06-30 23:00:42 +02:00
|
|
|
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
|
2024-06-28 19:03:20 +02:00
|
|
|
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:
|
2024-06-30 23:00:42 +02:00
|
|
|
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>
|
2024-06-28 19:03:20 +02:00
|
|
|
"""
|
2024-06-30 23:00:42 +02:00
|
|
|
return True, msg
|
2024-06-28 19:03:20 +02:00
|
|
|
|
|
|
|
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>'
|
2024-06-30 23:00:42 +02:00
|
|
|
return False, msg + "<p>(pas de notes modifiées)</p>"
|
2024-06-28 19:03:20 +02:00
|
|
|
|
|
|
|
|
2024-06-30 23:00:42 +02:00
|
|
|
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=()):
|
2024-06-28 19:03:20 +02:00
|
|
|
"""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(
|
2024-06-30 23:00:42 +02:00
|
|
|
f"""
|
2024-07-02 00:03:03 +02:00
|
|
|
<div class="notes-chargees">
|
|
|
|
<div><b>Notes chargées !</b></div>
|
|
|
|
{updiag[1]}
|
|
|
|
</div>
|
2024-06-28 19:03:20 +02:00
|
|
|
<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>
|
2024-06-30 23:00:42 +02:00
|
|
|
</div>"""
|
2024-06-28 19:03:20 +02:00
|
|
|
)
|
|
|
|
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>
|
|
|
|
"""
|
|
|
|
)
|
|
|
|
|
|
|
|
#
|
2024-07-02 00:03:03 +02:00
|
|
|
H.append("""</div>""")
|
2024-06-28 19:03:20 +02:00
|
|
|
if evaluation.moduleimpl.can_edit_notes(current_user, allow_ens=False):
|
|
|
|
H.append(
|
|
|
|
f"""
|
2024-07-02 00:03:03 +02:00
|
|
|
<div class="scobox explanation">
|
|
|
|
<div class="scobox-title">Autres opérations</div>
|
|
|
|
<div>
|
|
|
|
<ul>
|
2024-06-28 19:03:20 +02:00
|
|
|
<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>
|
2024-07-02 00:03:03 +02:00
|
|
|
</div>
|
|
|
|
</div>
|
2024-06-28 19:03:20 +02:00
|
|
|
|
2024-07-02 00:03:03 +02:00
|
|
|
<div class="scobox explanation">
|
|
|
|
<div class="scobox-title">Explications</div>
|
|
|
|
<div>
|
2024-06-28 19:03:20 +02:00
|
|
|
<ol>
|
2024-07-02 00:03:03 +02:00
|
|
|
<li><b>Etape 1</b>:
|
2024-06-28 19:03:20 +02:00
|
|
|
<ol><li>choisir le ou les groupes d'étudiants;</li>
|
|
|
|
<li>télécharger le fichier Excel à remplir.</li>
|
|
|
|
</ol>
|
|
|
|
</li>
|
2024-07-02 00:03:03 +02:00
|
|
|
<li><b>Etape 2</b> (cadre vert): Indiquer le fichier Excel
|
2024-06-28 19:03:20 +02:00
|
|
|
<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>
|
2024-07-02 00:03:03 +02:00
|
|
|
</div>
|
|
|
|
</div>
|
2024-06-28 19:03:20 +02:00
|
|
|
"""
|
|
|
|
)
|
|
|
|
H.append(html_sco_header.sco_footer())
|
|
|
|
return "\n".join(H)
|