##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""Fichier excel de saisie des notes
"""
from openpyxl.styles import Font, Border, Side, Alignment, PatternFill
from openpyxl.styles.numbers import FORMAT_GENERAL
from flask import g, request, url_for
from flask_login import current_user
from app.models import Evaluation, Identite, Module, ScolarNews
from app.scodoc.sco_excel import COLORS, ScoExcelSheet
from app.scodoc import (
html_sco_header,
sco_evaluations,
sco_excel,
sco_groups,
sco_groups_view,
sco_saisie_notes,
sco_users,
)
from app.scodoc.sco_exceptions import AccessDenied, InvalidNoteValue
import app.scodoc.sco_utils as scu
from app.scodoc.TrivialFormulator import TrivialFormulator
FONT_NAME = "Arial"
def excel_feuille_saisie(
evaluation: "Evaluation", titreannee, description, rows: list[dict]
):
"""Genere feuille excel pour saisie des notes.
E: evaluation (dict)
lines: liste de tuples
(etudid, nom, prenom, etat, groupe, val, explanation)
"""
sheet_name = "Saisie notes"
ws = ScoExcelSheet(sheet_name)
# fontes
font_base = Font(name=FONT_NAME, size=12)
font_bold = Font(name=FONT_NAME, bold=True)
font_italic = Font(name=FONT_NAME, size=12, italic=True, color=COLORS.RED.value)
font_titre = Font(name=FONT_NAME, bold=True, size=14)
font_purple = Font(name=FONT_NAME, color=COLORS.PURPLE.value)
font_brown = Font(name=FONT_NAME, color=COLORS.BROWN.value)
font_blue = Font(name=FONT_NAME, size=9, color=COLORS.BLUE.value)
# bordures
side_thin = Side(border_style="thin", color=COLORS.BLACK.value)
border_top = Border(top=side_thin)
border_right = Border(right=side_thin)
# fonds
fill_light_yellow = PatternFill(
patternType="solid", fgColor=COLORS.LIGHT_YELLOW.value
)
# styles
styles = {
"base": {"font": font_base},
"titres": {"font": font_titre},
"explanation": {"font": font_italic},
"read-only": { # cells read-only
"font": font_purple,
"border": border_right,
},
"dem": {
"font": font_brown,
"border": border_top,
},
"nom": { # style pour nom, prenom, groupe
"font": font_base,
"border": border_top,
},
"notes": {
"alignment": Alignment(horizontal="right"),
"font": font_bold,
"number_format": FORMAT_GENERAL,
"fill": fill_light_yellow,
"border": border_top,
},
"comment": {
"font": font_blue,
"border": border_top,
},
}
# filtre auto excel sur colonnes
filter_top = 8
filter_bottom = 8 + len(rows)
filter_left = "A" # important: le code etudid en col A doit être trié en même temps
filter_right = "G"
ws.set_auto_filter(f"${filter_left}${filter_top}:${filter_right}${filter_bottom}")
# ligne de titres (utilise prefix pour se placer à partir de la colonne B)
ws.append_single_cell_row(
"Feuille saisie note (à enregistrer au format excel)",
styles["titres"],
prefix=[""],
)
# lignes d'instructions
ws.append_single_cell_row(
"Saisir les notes dans la colonne E (cases jaunes)",
styles["explanation"],
prefix=[""],
)
ws.append_single_cell_row(
"Ne pas modifier les cases en mauve !", styles["explanation"], prefix=[""]
)
# Nom du semestre
ws.append_single_cell_row(
scu.unescape_html(titreannee), styles["titres"], prefix=[""]
)
# description evaluation
ws.append_single_cell_row(
scu.unescape_html(description), styles["titres"], prefix=[""]
)
ws.append_single_cell_row(
f"Evaluation {evaluation.descr_date()} (coef. {(evaluation.coefficient):g})",
styles["base"],
prefix=[""],
)
# ligne blanche
ws.append_blank_row()
# code et titres colonnes
ws.append_row(
[
ws.make_cell(f"!{evaluation.id}", styles["read-only"]),
ws.make_cell("Nom", styles["titres"]),
ws.make_cell("Prénom", styles["titres"]),
ws.make_cell("Groupe", styles["titres"]),
ws.make_cell(
f"Note sur {(evaluation.note_max or 0.0):g}", styles["titres"]
),
ws.make_cell("Remarque", styles["titres"]),
ws.make_cell("NIP", styles["titres"]),
]
)
# etudiants
for row in rows:
st = styles["nom"]
if row["etat"] != scu.INSCRIT:
st = styles["dem"]
if row["etat"] == scu.DEMISSION: # demissionnaire
groupe_ou_etat = "DEM"
else:
groupe_ou_etat = row["etat"] # etat autre
else:
groupe_ou_etat = row["groupes"] # groupes TD/TP/...
try:
note_str = float(row["note"]) # export numérique excel
except ValueError:
note_str = row["note"] # "ABS", ...
ws.append_row(
[
ws.make_cell("!" + row["etudid"], styles["read-only"]),
ws.make_cell(row["nom"], st),
ws.make_cell(row["prenom"], st),
ws.make_cell(groupe_ou_etat, st),
ws.make_cell(note_str, styles["notes"]), # note
ws.make_cell(row["explanation"], styles["comment"]), # comment
ws.make_cell(row["code_nip"], styles["read-only"]),
]
)
# ligne blanche
ws.append_blank_row()
# explication en bas
_insert_bottom_help(ws, styles)
ws.set_column_dimension_hidden("A", True) # colonne etudid cachée
ws.set_column_dimension_hidden("G", True) # colonne NIP cachée
return ws.generate(
column_widths={
"A": 11.0 / 7, # codes
"B": 164.00 / 7, # noms
"C": 109.0 / 7, # prenoms
"D": "auto", # groupes
"E": 115.0 / 7, # notes
"F": 355.0 / 7, # remarques
"G": 72.0 / 7, # colonne NIP
}
)
def _insert_bottom_help(ws, styles: dict):
ws.append_row([None, ws.make_cell("Code notes", styles["titres"])])
ws.append_row(
[
None,
ws.make_cell("ABS", styles["explanation"]),
ws.make_cell("absent (0)", styles["explanation"]),
]
)
ws.append_row(
[
None,
ws.make_cell("EXC", styles["explanation"]),
ws.make_cell("pas prise en compte", styles["explanation"]),
]
)
ws.append_row(
[
None,
ws.make_cell("ATT", styles["explanation"]),
ws.make_cell("en attente", styles["explanation"]),
]
)
ws.append_row(
[
None,
ws.make_cell("SUPR", styles["explanation"]),
ws.make_cell("pour supprimer note déjà entrée", styles["explanation"]),
]
)
ws.append_row(
[
None,
ws.make_cell("", styles["explanation"]),
ws.make_cell("cellule vide -> note non modifiée", styles["explanation"]),
]
)
def feuille_saisie_notes(evaluation_id, group_ids: list[int] = None):
"""Vue: document Excel pour saisie notes dans l'évaluation et les groupes indiqués"""
evaluation = Evaluation.get_evaluation(evaluation_id)
group_ids = group_ids or []
modimpl = evaluation.moduleimpl
formsemestre = modimpl.formsemestre
mod_responsable = sco_users.user_info(modimpl.responsable_id)
if evaluation.date_debut:
indication_date = evaluation.date_debut.date().isoformat()
else:
indication_date = scu.sanitize_filename(evaluation.description)[:12]
eval_name = f"{evaluation.moduleimpl.module.code}-{indication_date}"
date_str = (
f"""du {evaluation.date_debut.strftime(scu.DATE_FMT)}"""
if evaluation.date_debut
else "(sans date)"
)
eval_titre = f"""{evaluation.description if evaluation.description else "évaluation"
} {date_str}"""
description = f"""{eval_titre} en {evaluation.moduleimpl.module.abbrev or ""} ({
evaluation.moduleimpl.module.code
}) resp. {mod_responsable["prenomnom"]}"""
groups_infos = sco_groups_view.DisplayedGroupsInfos(
group_ids=group_ids,
formsemestre_id=formsemestre.id,
select_all_when_unspecified=True,
etat=None,
)
groups = sco_groups.listgroups(groups_infos.group_ids)
gr_title_filename = sco_groups.listgroups_filename(groups)
if None in [g["group_name"] for g in groups]: # tous les etudiants
getallstudents = True
gr_title_filename = "tous"
else:
getallstudents = False
etudids = [
x[0]
for x in sco_groups.do_evaluation_listeetuds_groups(
evaluation_id, groups, getallstudents=getallstudents, include_demdef=True
)
]
# une liste de liste de chaines: lignes de la feuille de calcul
rows = []
etuds = sco_saisie_notes.get_sorted_etuds_notes(
evaluation, etudids, formsemestre.id
)
for e in etuds:
etudid = e["etudid"]
groups = sco_groups.get_etud_groups(etudid, formsemestre.id)
grc = sco_groups.listgroups_abbrev(groups)
rows.append(
{
"etudid": str(etudid),
"code_nip": e["code_nip"],
"explanation": e["explanation"],
"nom": e.get("nom_disp", "") or e.get("nom_usuel", "") or e["nom"],
"prenom": e["prenom"].lower().capitalize(),
"etat": e["inscr"]["etat"],
"groupes": grc,
"note": e["val"],
}
)
filename = f"notes_{eval_name}_{gr_title_filename}"
xls = excel_feuille_saisie(
evaluation, formsemestre.titre_annee(), description, rows=rows
)
return scu.send_file(xls, filename, scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE)
def do_evaluation_upload_xls():
"""
Soumission d'un fichier XLS (evaluation_id, notefile)
"""
args = scu.get_request_args()
evaluation_id = int(args["evaluation_id"])
comment = args["comment"]
evaluation = Evaluation.get_evaluation(evaluation_id)
# Check access (admin, respformation, responsable_id, ens)
if not evaluation.moduleimpl.can_edit_notes(current_user):
raise AccessDenied(f"Modification des notes impossible pour {current_user}")
#
diag, lines = sco_excel.excel_file_to_list(args["notefile"])
try:
if not lines:
raise InvalidNoteValue()
# -- search eval code
n = len(lines)
i = 0
while i < n:
if not lines[i]:
diag.append("Erreur: format invalide (ligne vide ?)")
raise InvalidNoteValue()
f0 = lines[i][0].strip()
if f0 and f0[0] == "!":
break
i = i + 1
if i == n:
diag.append("Erreur: format invalide ! (pas de ligne evaluation_id)")
raise InvalidNoteValue()
eval_id_str = lines[i][0].strip()[1:]
try:
eval_id = int(eval_id_str)
except ValueError:
eval_id = None
if eval_id != evaluation_id:
diag.append(
f"""Erreur: fichier invalide: le code d'évaluation de correspond pas ! ('{
eval_id_str}' != '{evaluation_id}')"""
)
raise InvalidNoteValue()
# --- get notes -> list (etudid, value)
# ignore toutes les lignes ne commençant pas par !
notes = []
ni = i + 1
try:
for line in lines[i + 1 :]:
if line:
cell0 = line[0].strip()
if cell0 and cell0[0] == "!":
etudid = cell0[1:]
if len(line) > 4:
val = line[4].strip()
else:
val = "" # ligne courte: cellule vide
if etudid:
notes.append((etudid, val))
ni += 1
except Exception as exc:
diag.append(
f"""Erreur: Ligne invalide ! (erreur ligne {ni})
{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
{len(etudids_changed)} notes changées ({len(withoutnotes)} sans notes, {len(absents)} absents, {nb_suppress} note supprimées)
""" if messages: msg += f"""Important: il y avait déjà des décisions de jury enregistrées, qui sont peut-être à revoir suite à cette modification !
""" return 1, msg except InvalidNoteValue: if diag: msg = ( '(pas de notes modifiées)
" def saisie_notes_tableur(evaluation_id, group_ids=()): """Saisie des notes via un fichier Excel""" evaluation = Evaluation.query.get_or_404(evaluation_id) moduleimpl_id = evaluation.moduleimpl.id formsemestre_id = evaluation.moduleimpl.formsemestre_id if not evaluation.moduleimpl.can_edit_notes(current_user): return ( html_sco_header.sco_header() + f"""(vérifiez que le semestre n'est pas verrouillé et que vous avez l'autorisation d'effectuer cette opération)
""" + 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), """Saisie des notes par fichier""", ] # Menu choix groupe: H.append("""""") H.append(sco_groups_view.form_groups_choice(groups_infos)) H.append(" |
Le fichier doit être un fichier tableur obtenu via l'étape 1 ci-dessus, puis complété et enregistré au format Excel.
""" ) H.append(nf[1]) elif nf[0] == -1: H.append("Annulation
") elif nf[0] == 1: updiag = do_evaluation_upload_xls() if updiag[0]: H.append(updiag[1]) H.append( f"""Notes chargées. Revenir au tableau de bord du module Charger un autre fichier de notes Formulaire de saisie des notes
""" ) else: H.append( f"""Notes non chargées !
{updiag[1]} """ ) # H.append("""