forked from ScoDoc/ScoDoc
WIP: Import de toutes les notes d'un semestre: génération de la feuille. Début de #942.
This commit is contained in:
parent
4214a53a4e
commit
62e4481c77
@ -33,6 +33,7 @@ import io
|
|||||||
import time
|
import time
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
|
from typing import AnyStr
|
||||||
|
|
||||||
import openpyxl.utils.datetime
|
import openpyxl.utils.datetime
|
||||||
from openpyxl.styles.numbers import FORMAT_NUMBER_00, FORMAT_GENERAL, FORMAT_DATE_DDMMYY
|
from openpyxl.styles.numbers import FORMAT_NUMBER_00, FORMAT_GENERAL, FORMAT_DATE_DDMMYY
|
||||||
@ -250,10 +251,10 @@ class ScoExcelSheet:
|
|||||||
return idx
|
return idx
|
||||||
if idx < 26: # one letter key
|
if idx < 26: # one letter key
|
||||||
return chr(idx + 65)
|
return chr(idx + 65)
|
||||||
else: # two letters AA..ZZ
|
# two letters AA..ZZ
|
||||||
first = (idx // 26) + 66
|
first = (idx // 26) + 64
|
||||||
second = (idx % 26) + 65
|
second = (idx % 26) + 65
|
||||||
return "" + chr(first) + chr(second)
|
return "" + chr(first) + chr(second)
|
||||||
|
|
||||||
def set_column_dimension_width(self, cle=None, value: int | str | list = 21):
|
def set_column_dimension_width(self, cle=None, value: int | str | list = 21):
|
||||||
"""Détermine la largeur d'une colonne.
|
"""Détermine la largeur d'une colonne.
|
||||||
@ -295,7 +296,7 @@ class ScoExcelSheet:
|
|||||||
self.ws.row_dimensions[cle].hidden = value
|
self.ws.row_dimensions[cle].hidden = value
|
||||||
|
|
||||||
def set_column_dimension_hidden(self, cle, value):
|
def set_column_dimension_hidden(self, cle, value):
|
||||||
"""Masque ou affiche une ligne.
|
"""Masque ou affiche une colonne.
|
||||||
cle -- identifie la colonne (1...)
|
cle -- identifie la colonne (1...)
|
||||||
value -- boolean (vrai = colonne cachée)
|
value -- boolean (vrai = colonne cachée)
|
||||||
"""
|
"""
|
||||||
@ -331,6 +332,7 @@ class ScoExcelSheet:
|
|||||||
else:
|
else:
|
||||||
min_row = self.ws.min_row if min_row is None else min_row
|
min_row = self.ws.min_row if min_row is None else min_row
|
||||||
max_row = self.ws.max_row if max_row is None else max_row
|
max_row = self.ws.max_row if max_row is None else max_row
|
||||||
|
|
||||||
for row in range(min_row, max_row + 1):
|
for row in range(min_row, max_row + 1):
|
||||||
cell = self.ws[f"{column_letter}{row}"]
|
cell = self.ws[f"{column_letter}{row}"]
|
||||||
cell_value = str(cell.value)
|
cell_value = str(cell.value)
|
||||||
@ -339,10 +341,13 @@ class ScoExcelSheet:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Set the column widths based on the maximum length found
|
# Set the column widths based on the maximum length found
|
||||||
|
# (nb: the width is expressed in characters, in the default font)
|
||||||
for col, width in col_widths.items():
|
for col, width in col_widths.items():
|
||||||
self.ws.column_dimensions[col].width = width
|
self.ws.column_dimensions[col].width = width
|
||||||
|
|
||||||
def make_cell(self, value: any = None, style: dict = None, comment=None):
|
def make_cell(
|
||||||
|
self, value: any = None, style: dict = None, comment=None
|
||||||
|
) -> WriteOnlyCell:
|
||||||
"""Construit une cellule.
|
"""Construit une cellule.
|
||||||
value -- contenu de la cellule (texte, numérique, booléen ou date)
|
value -- contenu de la cellule (texte, numérique, booléen ou date)
|
||||||
style -- style par défaut (dictionnaire cf. excel_make_style) de la feuille si non spécifié
|
style -- style par défaut (dictionnaire cf. excel_make_style) de la feuille si non spécifié
|
||||||
@ -434,7 +439,7 @@ class ScoExcelSheet:
|
|||||||
for row in self.rows:
|
for row in self.rows:
|
||||||
self.ws.append(row)
|
self.ws.append(row)
|
||||||
|
|
||||||
def generate(self, column_widths=None):
|
def generate(self, column_widths=None) -> AnyStr:
|
||||||
"""génération d'un classeur mono-feuille"""
|
"""génération d'un classeur mono-feuille"""
|
||||||
# this method makes sense for standalone worksheet (else call workbook.generate())
|
# this method makes sense for standalone worksheet (else call workbook.generate())
|
||||||
if self.wb is None: # embeded sheet
|
if self.wb is None: # embeded sheet
|
||||||
|
@ -427,6 +427,12 @@ def formsemestre_status_menubar(formsemestre: FormSemestre | None) -> str:
|
|||||||
"endpoint": "notes.formsemestre_list_saisies_notes",
|
"endpoint": "notes.formsemestre_list_saisies_notes",
|
||||||
"args": {"formsemestre_id": formsemestre_id},
|
"args": {"formsemestre_id": formsemestre_id},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Importer les notes",
|
||||||
|
"endpoint": "notes.formsemestre_import_notes",
|
||||||
|
"args": {"formsemestre_id": formsemestre_id},
|
||||||
|
"enabled": formsemestre.est_chef_or_diretud(),
|
||||||
|
},
|
||||||
]
|
]
|
||||||
menu_jury = [
|
menu_jury = [
|
||||||
{
|
{
|
||||||
|
@ -25,17 +25,20 @@
|
|||||||
"""Fichier excel de saisie des notes
|
"""Fichier excel de saisie des notes
|
||||||
"""
|
"""
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from openpyxl.styles import Font, Border, Side, Alignment, PatternFill
|
from typing import AnyStr
|
||||||
|
|
||||||
|
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 flash, g, request, url_for
|
from flask import g, request, url_for
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
|
||||||
from app.models import Evaluation, Identite, Module, ScolarNews
|
from app.models import Evaluation, FormSemestre, Identite, Module, 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_evaluations,
|
sco_evaluations,
|
||||||
|
sco_evaluation_db,
|
||||||
sco_excel,
|
sco_excel,
|
||||||
sco_groups,
|
sco_groups,
|
||||||
sco_groups_view,
|
sco_groups_view,
|
||||||
@ -49,14 +52,14 @@ from app.scodoc.TrivialFormulator import TrivialFormulator
|
|||||||
FONT_NAME = "Arial"
|
FONT_NAME = "Arial"
|
||||||
|
|
||||||
|
|
||||||
def excel_feuille_saisie(evaluation: "Evaluation", rows: list[dict]):
|
def excel_feuille_saisie(evaluation: "Evaluation", rows: list[dict]) -> AnyStr:
|
||||||
"""Genere feuille excel pour saisie des notes.
|
"""Génère feuille excel pour saisie des notes dans l'evaluation
|
||||||
E: evaluation (dict)
|
- evaluation
|
||||||
lines: liste de tuples
|
- rows: liste de dict
|
||||||
(etudid, nom, prenom, etat, groupe, val, explanation)
|
(etudid, nom, prenom, etat, groupe, val, explanation)
|
||||||
|
Return excel data.
|
||||||
"""
|
"""
|
||||||
sheet_name = "Saisie notes"
|
ws = ScoExcelSheet("Saisie notes")
|
||||||
ws = ScoExcelSheet(sheet_name)
|
|
||||||
styles = _build_styles()
|
styles = _build_styles()
|
||||||
nb_lines_titles = _insert_top_title(ws, styles, evaluation=evaluation)
|
nb_lines_titles = _insert_top_title(ws, styles, evaluation=evaluation)
|
||||||
|
|
||||||
@ -100,6 +103,8 @@ def excel_feuille_saisie(evaluation: "Evaluation", rows: list[dict]):
|
|||||||
|
|
||||||
# explication en bas
|
# explication en bas
|
||||||
_insert_bottom_help(ws, styles)
|
_insert_bottom_help(ws, styles)
|
||||||
|
|
||||||
|
# Hide column A (codes étudiants)
|
||||||
ws.set_column_dimension_hidden("A", True) # colonne etudid cachée
|
ws.set_column_dimension_hidden("A", True) # colonne etudid cachée
|
||||||
ws.set_column_dimension_hidden("G", True) # colonne NIP cachée
|
ws.set_column_dimension_hidden("G", True) # colonne NIP cachée
|
||||||
|
|
||||||
@ -122,27 +127,46 @@ def _insert_line_titles(
|
|||||||
nb_rows_in_table: int = 0,
|
nb_rows_in_table: int = 0,
|
||||||
evaluations: list[Evaluation] = None,
|
evaluations: list[Evaluation] = None,
|
||||||
styles: dict = None,
|
styles: dict = None,
|
||||||
) -> int:
|
multi_eval=False,
|
||||||
"""Ligne(s) des titres, avec filtre auto excel.
|
) -> dict:
|
||||||
|
"""Insère ligne des titres, avec filtre auto excel.
|
||||||
current_line : nb de lignes déjà dans le tableau
|
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)
|
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
|
multi_eval: si vrai, titres pour plusieurs évaluations (feuille import semestre)
|
||||||
ids au dessus des titres)
|
|
||||||
|
Return dict giving (title) column widths
|
||||||
"""
|
"""
|
||||||
# WIP
|
# La colonne de gauche (utilisée pour cadrer le filtre)
|
||||||
assert len(evaluations) == 1
|
# est G si une seule eval
|
||||||
evaluation = evaluations[0]
|
right_column = ScoExcelSheet.i2col(3 + len(evaluations)) if multi_eval else "G"
|
||||||
|
|
||||||
# Filtre auto excel sur colonnes
|
# Filtre auto excel sur colonnes
|
||||||
filter_top = current_line + 1
|
filter_top = current_line + 1
|
||||||
filter_bottom = current_line + 1 + nb_rows_in_table
|
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_left = "A" # important: le code etudid en col A doit être trié en même temps
|
||||||
filter_right = "G"
|
filter_right = right_column
|
||||||
ws.set_auto_filter(f"${filter_left}${filter_top}:${filter_right}${filter_bottom}")
|
ws.set_auto_filter(f"${filter_left}${filter_top}:${filter_right}${filter_bottom}")
|
||||||
|
|
||||||
# Code et titres colonnes
|
# Code et titres colonnes
|
||||||
ws.append_row(
|
if multi_eval:
|
||||||
[
|
cells = [
|
||||||
|
ws.make_cell("", 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"""{evaluation.moduleimpl.module.code
|
||||||
|
} : {evaluation.description} (/{(evaluation.note_max or 0.0):g})""",
|
||||||
|
styles["titres"],
|
||||||
|
comment=f"""{evaluation.descr_date()
|
||||||
|
}, notes sur {(evaluation.note_max or 0.0):g}""",
|
||||||
|
)
|
||||||
|
for evaluation in evaluations
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
evaluation = evaluations[0]
|
||||||
|
cells = [
|
||||||
ws.make_cell(f"!{evaluation.id}", styles["read-only"]),
|
ws.make_cell(f"!{evaluation.id}", styles["read-only"]),
|
||||||
ws.make_cell("Nom", styles["titres"]),
|
ws.make_cell("Nom", styles["titres"]),
|
||||||
ws.make_cell("Prénom", styles["titres"]),
|
ws.make_cell("Prénom", styles["titres"]),
|
||||||
@ -153,21 +177,35 @@ def _insert_line_titles(
|
|||||||
ws.make_cell("Remarque", styles["titres"]),
|
ws.make_cell("Remarque", styles["titres"]),
|
||||||
ws.make_cell("NIP", styles["titres"]),
|
ws.make_cell("NIP", styles["titres"]),
|
||||||
]
|
]
|
||||||
)
|
ws.append_row(cells)
|
||||||
return 1 # WIP
|
|
||||||
|
# Calcul largeur colonnes (actuellement pour feuille import multi seulement)
|
||||||
|
# Le facteur prend en compte la tailel du font (14)
|
||||||
|
font_size_factor = 1.25
|
||||||
|
column_widths = {
|
||||||
|
ScoExcelSheet.i2col(idx): (len(str(cell.value)) + 2.0) * font_size_factor
|
||||||
|
for idx, cell in enumerate(cells)
|
||||||
|
}
|
||||||
|
# Force largeurs des colonnes noms/prénoms/groupes
|
||||||
|
column_widths["B"] = 26.0 # noms
|
||||||
|
column_widths["C"] = 26.0 # noms
|
||||||
|
column_widths["D"] = 26.0 # groupes
|
||||||
|
|
||||||
|
return column_widths
|
||||||
|
|
||||||
|
|
||||||
def _build_styles() -> dict:
|
def _build_styles() -> dict:
|
||||||
"""Déclare le styles excel"""
|
"""Déclare les styles excel"""
|
||||||
|
|
||||||
# bordures
|
# bordures
|
||||||
side_thin = Side(border_style="thin", color=COLORS.BLACK.value)
|
side_thin = Side(border_style="thin", color=Color(rgb="666688"))
|
||||||
border_top = Border(top=side_thin)
|
border_top = Border(top=side_thin)
|
||||||
|
border_box = Border(
|
||||||
|
top=side_thin, left=side_thin, bottom=side_thin, right=side_thin
|
||||||
|
)
|
||||||
|
|
||||||
# fonds
|
# fonds
|
||||||
fill_light_yellow = PatternFill(
|
fill_saisie_notes = PatternFill(patternType="solid", fgColor=Color(rgb="E3FED4"))
|
||||||
patternType="solid", fgColor=COLORS.LIGHT_YELLOW.value
|
|
||||||
)
|
|
||||||
|
|
||||||
# styles
|
# styles
|
||||||
font_base = Font(name=FONT_NAME, size=12)
|
font_base = Font(name=FONT_NAME, size=12)
|
||||||
@ -179,22 +217,22 @@ def _build_styles() -> dict:
|
|||||||
},
|
},
|
||||||
"read-only": { # cells read-only
|
"read-only": { # cells read-only
|
||||||
"font": Font(name=FONT_NAME, color=COLORS.PURPLE.value),
|
"font": Font(name=FONT_NAME, color=COLORS.PURPLE.value),
|
||||||
"border": Border(right=side_thin),
|
"border": border_box,
|
||||||
},
|
},
|
||||||
"dem": {
|
"dem": {
|
||||||
"font": Font(name=FONT_NAME, color=COLORS.BROWN.value),
|
"font": Font(name=FONT_NAME, color=COLORS.BROWN.value),
|
||||||
"border": border_top,
|
"border": border_box,
|
||||||
},
|
},
|
||||||
"nom": { # style pour nom, prenom, groupe
|
"nom": { # style pour nom, prenom, groupe
|
||||||
"font": font_base,
|
"font": font_base,
|
||||||
"border": border_top,
|
"border": border_box,
|
||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"alignment": Alignment(horizontal="right"),
|
"alignment": Alignment(horizontal="right"),
|
||||||
"font": Font(name=FONT_NAME, bold=True),
|
"font": Font(name=FONT_NAME, bold=False),
|
||||||
"number_format": FORMAT_GENERAL,
|
"number_format": FORMAT_GENERAL,
|
||||||
"fill": fill_light_yellow,
|
"fill": fill_saisie_notes,
|
||||||
"border": border_top,
|
"border": border_box,
|
||||||
},
|
},
|
||||||
"comment": {
|
"comment": {
|
||||||
"font": Font(name=FONT_NAME, size=9, color=COLORS.BLUE.value),
|
"font": Font(name=FONT_NAME, size=9, color=COLORS.BLUE.value),
|
||||||
@ -204,9 +242,15 @@ def _build_styles() -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def _insert_top_title(
|
def _insert_top_title(
|
||||||
ws, styles: dict, evaluation: Evaluation = None, description=""
|
ws,
|
||||||
|
styles: dict,
|
||||||
|
evaluation: Evaluation | None = None,
|
||||||
|
formsemestre: FormSemestre | None = None,
|
||||||
|
description="",
|
||||||
) -> int:
|
) -> int:
|
||||||
"""Insère les lignes de titre de la feuille (suivies d'une ligne blanche)
|
"""Insère les lignes de titre de la feuille (suivies d'une ligne blanche).
|
||||||
|
Si evaluation, indique son titre.
|
||||||
|
Si formsemestre, indique son titre.
|
||||||
renvoie le nb de lignes insérées
|
renvoie le nb de lignes insérées
|
||||||
"""
|
"""
|
||||||
n = 0
|
n = 0
|
||||||
@ -219,42 +263,54 @@ def _insert_top_title(
|
|||||||
n += 1
|
n += 1
|
||||||
# lignes d'instructions
|
# lignes d'instructions
|
||||||
ws.append_single_cell_row(
|
ws.append_single_cell_row(
|
||||||
"Saisir les notes dans la colonne E (cases jaunes)",
|
(
|
||||||
|
"Saisir les notes dans la colonne E (cases vertes)"
|
||||||
|
if evaluation
|
||||||
|
else "Saisir les notes de chaque évaluation"
|
||||||
|
),
|
||||||
styles["explanation"],
|
styles["explanation"],
|
||||||
prefix=[""],
|
prefix=[""],
|
||||||
)
|
)
|
||||||
ws.append_single_cell_row(
|
ws.append_single_cell_row(
|
||||||
"Ne pas modifier les cases en mauve !", styles["explanation"], prefix=[""]
|
"Ne pas modifier les lignes et colonnes masquées (en mauve)!",
|
||||||
|
styles["explanation"],
|
||||||
|
prefix=[""],
|
||||||
)
|
)
|
||||||
n += 2
|
n += 2
|
||||||
# Nom du semestre
|
# Nom du semestre
|
||||||
titre_annee = evaluation.moduleimpl.formsemestre.titre_annee()
|
titre_annee = (
|
||||||
|
evaluation.moduleimpl.formsemestre.titre_annee()
|
||||||
|
if evaluation
|
||||||
|
else (formsemestre.titre_annee() if formsemestre else "")
|
||||||
|
)
|
||||||
ws.append_single_cell_row(
|
ws.append_single_cell_row(
|
||||||
scu.unescape_html(titre_annee), styles["titres"], prefix=[""]
|
scu.unescape_html(titre_annee), styles["titres"], prefix=[""]
|
||||||
)
|
)
|
||||||
n += 1
|
n += 1
|
||||||
# description evaluation
|
# description evaluation
|
||||||
date_str = (
|
if evaluation:
|
||||||
f"""du {evaluation.date_debut.strftime(scu.DATE_FMT)}"""
|
date_str = (
|
||||||
if evaluation.date_debut
|
f"""du {evaluation.date_debut.strftime(scu.DATE_FMT)}"""
|
||||||
else "(sans date)"
|
if evaluation.date_debut
|
||||||
)
|
else "(sans date)"
|
||||||
eval_titre = f"""{evaluation.description if evaluation.description else "évaluation"
|
)
|
||||||
} {date_str}"""
|
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
|
||||||
|
|
||||||
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
|
# ligne blanche
|
||||||
ws.append_blank_row()
|
ws.append_blank_row()
|
||||||
n += 1
|
n += 1
|
||||||
@ -300,7 +356,9 @@ def _insert_bottom_help(ws, styles: dict):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def feuille_saisie_notes(evaluation_id, group_ids: list[int] = None):
|
def feuille_saisie_notes(
|
||||||
|
evaluation_id, group_ids: list[int] = None
|
||||||
|
): # TODO ré-écrire et passer dans notes.py
|
||||||
"""Vue: document Excel pour saisie notes dans l'évaluation et les groupes indiqués"""
|
"""Vue: document Excel pour saisie notes dans l'évaluation et les groupes indiqués"""
|
||||||
evaluation = Evaluation.get_evaluation(evaluation_id)
|
evaluation = Evaluation.get_evaluation(evaluation_id)
|
||||||
group_ids = group_ids or []
|
group_ids = group_ids or []
|
||||||
@ -360,6 +418,113 @@ def feuille_saisie_notes(evaluation_id, group_ids: list[int] = None):
|
|||||||
return scu.send_file(xls, filename, scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE)
|
return scu.send_file(xls, filename, scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE)
|
||||||
|
|
||||||
|
|
||||||
|
def excel_feuille_import(formsemestre: FormSemestre) -> AnyStr:
|
||||||
|
"""Génère feuille pour import toutes notes dans ce semestre,
|
||||||
|
avec une colonne par évaluation.
|
||||||
|
Return excel data
|
||||||
|
"""
|
||||||
|
evaluations = formsemestre.get_evaluations()
|
||||||
|
etudiants = formsemestre.get_inscrits(include_demdef=True, order=True)
|
||||||
|
rows = [{"etud": etud} for etud in etudiants]
|
||||||
|
# Liste les étudiants et leur note à chaque évaluation
|
||||||
|
for evaluation in evaluations:
|
||||||
|
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation.id)
|
||||||
|
inscrits_module = {ins.etudid for ins in evaluation.moduleimpl.inscriptions}
|
||||||
|
for row in rows:
|
||||||
|
etud = row["etud"]
|
||||||
|
if not etud.id in inscrits_module:
|
||||||
|
note_str = "NI" # non inscrit à ce module
|
||||||
|
else:
|
||||||
|
val = notes_db.get(etud.id, {}).get("value", "")
|
||||||
|
# export numérique excel
|
||||||
|
note_str = scu.fmt_note(val, keep_numeric=True)
|
||||||
|
|
||||||
|
row[evaluation.id] = note_str
|
||||||
|
#
|
||||||
|
return generate_excel_import_notes(evaluations, rows)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_excel_import_notes(
|
||||||
|
evaluations: list[Evaluation], rows: list[dict]
|
||||||
|
) -> AnyStr:
|
||||||
|
"""Génère la feuille excel pour l'import multi-évaluations.
|
||||||
|
On distingue ces feuille de celles utilisées pour une seule éval par la présence
|
||||||
|
de la valeur "MULTIEVAL" en tête de la colonne A (qui est invisible).
|
||||||
|
"""
|
||||||
|
ws = ScoExcelSheet("Import notes")
|
||||||
|
styles = _build_styles()
|
||||||
|
formsemestre: FormSemestre = (
|
||||||
|
evaluations[0].moduleimpl.formsemestre if evaluations else None
|
||||||
|
)
|
||||||
|
nb_lines_titles = _insert_top_title(ws, styles, formsemestre=formsemestre)
|
||||||
|
|
||||||
|
# codes évaluations
|
||||||
|
ws.append_row(
|
||||||
|
[
|
||||||
|
ws.make_cell(x, styles["read-only"])
|
||||||
|
for x in [
|
||||||
|
"MULTIEVAL",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
]
|
||||||
|
+ [evaluation.id for evaluation in evaluations]
|
||||||
|
)
|
||||||
|
column_widths = _insert_line_titles(
|
||||||
|
ws,
|
||||||
|
nb_lines_titles + 1,
|
||||||
|
nb_rows_in_table=len(rows),
|
||||||
|
evaluations=evaluations,
|
||||||
|
styles=styles,
|
||||||
|
multi_eval=True,
|
||||||
|
)
|
||||||
|
if not formsemestre: # aucune évaluation
|
||||||
|
rows = []
|
||||||
|
# etudiants
|
||||||
|
etuds_inscriptions = formsemestre.etuds_inscriptions
|
||||||
|
for row in rows:
|
||||||
|
etud: Identite = row["etud"]
|
||||||
|
st = styles["nom"]
|
||||||
|
match etuds_inscriptions[etud.id].etat:
|
||||||
|
case scu.INSCRIT:
|
||||||
|
groups = sco_groups.get_etud_groups(etud.id, formsemestre.id)
|
||||||
|
groupe_ou_etat = sco_groups.listgroups_abbrev(groups)
|
||||||
|
case scu.DEMISSION:
|
||||||
|
st = styles["dem"]
|
||||||
|
groupe_ou_etat = "DEM"
|
||||||
|
case scu.DEF:
|
||||||
|
groupe_ou_etat = "DEF"
|
||||||
|
st = styles["dem"]
|
||||||
|
case _:
|
||||||
|
groupe_ou_etat = "?" # état inconnu
|
||||||
|
ws.append_row(
|
||||||
|
[
|
||||||
|
ws.make_cell("!" + str(etud.id), styles["read-only"]),
|
||||||
|
ws.make_cell(etud.nom_disp(), st),
|
||||||
|
ws.make_cell(etud.prenom_str, st),
|
||||||
|
ws.make_cell(groupe_ou_etat, st),
|
||||||
|
]
|
||||||
|
+ [
|
||||||
|
ws.make_cell(row[evaluation.id], styles["notes"])
|
||||||
|
for evaluation in evaluations
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# ligne blanche
|
||||||
|
ws.append_blank_row()
|
||||||
|
|
||||||
|
# explication en bas
|
||||||
|
_insert_bottom_help(ws, styles)
|
||||||
|
|
||||||
|
# Hide column A (codes étudiants)
|
||||||
|
ws.set_column_dimension_hidden("A", True)
|
||||||
|
# Hide row codes evaluations
|
||||||
|
ws.set_row_dimension_hidden(nb_lines_titles + 1, True)
|
||||||
|
|
||||||
|
return ws.generate(column_widths=column_widths)
|
||||||
|
|
||||||
|
|
||||||
def do_evaluation_upload_xls() -> tuple[bool, str]:
|
def do_evaluation_upload_xls() -> tuple[bool, str]:
|
||||||
"""
|
"""
|
||||||
Soumission d'un fichier XLS (evaluation_id, notefile)
|
Soumission d'un fichier XLS (evaluation_id, notefile)
|
||||||
@ -652,7 +817,7 @@ def saisie_notes_tableur(evaluation_id: int, group_ids=()):
|
|||||||
|
|
||||||
H.append(
|
H.append(
|
||||||
f"""<div class="saisienote_etape1">
|
f"""<div class="saisienote_etape1">
|
||||||
<span class="titredivsaisienote">Etape 1 : </span>
|
<span class="titredivsaisienote">Étape 1 : </span>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a class="stdlink" href="feuille_saisie_notes?evaluation_id={evaluation_id}&{
|
<li><a class="stdlink" href="feuille_saisie_notes?evaluation_id={evaluation_id}&{
|
||||||
groups_infos.groups_query_args}"
|
groups_infos.groups_query_args}"
|
||||||
@ -672,7 +837,7 @@ def saisie_notes_tableur(evaluation_id: int, group_ids=()):
|
|||||||
|
|
||||||
H.append(
|
H.append(
|
||||||
"""<div class="saisienote_etape2">
|
"""<div class="saisienote_etape2">
|
||||||
<span class="titredivsaisienote">Etape 2 : chargement d'un fichier de notes</span>""" # '
|
<span class="titredivsaisienote">Étape 2 : chargement d'un fichier de notes</span>""" # '
|
||||||
)
|
)
|
||||||
|
|
||||||
nf = TrivialFormulator(
|
nf = TrivialFormulator(
|
||||||
|
50
app/templates/formsemestre/import_notes.j2
Normal file
50
app/templates/formsemestre/import_notes.j2
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
{% extends "sco_page.j2" %}
|
||||||
|
{% import 'wtf.j2' as wtf %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
{{super()}}
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
<h2>Import de notes dans les évaluations du semestre</h2>
|
||||||
|
|
||||||
|
<div class="help">
|
||||||
|
Cette page permet d'importer des notes dans tout ou partie des évaluations du semestre.
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
Il y a <a class="stdlink" href="{{
|
||||||
|
url_for('notes.evaluations_recap', scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id )
|
||||||
|
}}">{{ evaluations | length }} évaluations</a> définies dans ce semestre.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="saisienote_etape1">
|
||||||
|
<span class="titredivsaisienote">Étape 1 : </span>
|
||||||
|
<ul>
|
||||||
|
<li><a class="stdlink" href="{{
|
||||||
|
url_for( 'notes.feuille_import_notes', scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id )
|
||||||
|
}}" id="lnk_feuille_saisie">
|
||||||
|
obtenir le fichier tableur à remplir
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="help" style="margin-top: 24px;">Une fois que le fichier tableur exporté ci-dessus est rempli, téléchargez-le
|
||||||
|
ci-dessous.
|
||||||
|
Le texte "commentaire" sera associé à chaque note pour l'historique, il n'est jamais montré aux étudiants.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="saisienote_etape2">
|
||||||
|
<span class="titredivsaisienote">Étape 2 : chargement du fichier de notes</span>
|
||||||
|
|
||||||
|
{{ wtf.quick_form(form, enctype="multipart/form-data") }}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -38,6 +38,10 @@ import flask
|
|||||||
from flask import flash, redirect, render_template, url_for
|
from flask import flash, redirect, render_template, url_for
|
||||||
from flask import g, request
|
from flask import g, request
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from flask_wtf.file import FileAllowed
|
||||||
|
from wtforms.validators import DataRequired, Length
|
||||||
|
from wtforms import FileField, HiddenField, StringField, SubmitField
|
||||||
|
|
||||||
from app import db, log, send_scodoc_alarm
|
from app import db, log, send_scodoc_alarm
|
||||||
from app import models
|
from app import models
|
||||||
@ -1835,6 +1839,61 @@ sco_publish(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/formsemestre_import_notes/<int:formsemestre_id>", methods=["GET", "POST"])
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView) # controle contextuel
|
||||||
|
def formsemestre_import_notes(formsemestre_id: int):
|
||||||
|
"""Import via excel des notes de toutes les évals d'un semestre"""
|
||||||
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||||
|
dest_url = url_for(
|
||||||
|
"notes.formsemestre_status",
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
formsemestre_id=formsemestre.id,
|
||||||
|
)
|
||||||
|
if not formsemestre.est_chef_or_diretud():
|
||||||
|
raise ScoPermissionDenied("opération non autorisée", dest_url=dest_url)
|
||||||
|
|
||||||
|
class ImportForm(FlaskForm):
|
||||||
|
evaluation_id = HiddenField("formsemestre_id", default=formsemestre.id)
|
||||||
|
notefile = FileField(
|
||||||
|
"Fichier d'import",
|
||||||
|
validators=[
|
||||||
|
DataRequired(),
|
||||||
|
FileAllowed(["xlsx"], "Fichier xlsx seulement !"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
comment = StringField("Commentaire", validators=[Length(max=256)])
|
||||||
|
submit = SubmitField("Télécharger")
|
||||||
|
|
||||||
|
form = ImportForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
# Handle file upload and form processing
|
||||||
|
notefile = form.notefile.data
|
||||||
|
comment = form.comment.data
|
||||||
|
# Save the file and process form data here
|
||||||
|
raise ScoValueError("unimplemented")
|
||||||
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"formsemestre/import_notes.j2",
|
||||||
|
evaluations=formsemestre.get_evaluations(),
|
||||||
|
form=form,
|
||||||
|
formsemestre=formsemestre,
|
||||||
|
sco=ScoData(formsemestre=formsemestre),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/feuille_import_notes/<int:formsemestre_id>")
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def feuille_import_notes(formsemestre_id: int):
|
||||||
|
"""Feuille excel pour importer les notes de toutes les évaluations du semestre"""
|
||||||
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||||
|
xls = sco_saisie_excel.excel_feuille_import(formsemestre)
|
||||||
|
filename = scu.sanitize_filename(formsemestre.titre_annee())
|
||||||
|
return scu.send_file(xls, filename, scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE)
|
||||||
|
|
||||||
|
|
||||||
# --- Bulletins
|
# --- Bulletins
|
||||||
@bp.route("/formsemestre_bulletins_pdf")
|
@bp.route("/formsemestre_bulletins_pdf")
|
||||||
@scodoc
|
@scodoc
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# -*- mode: python -*-
|
# -*- mode: python -*-
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
SCOVERSION = "9.6.991"
|
SCOVERSION = "9.6.992"
|
||||||
|
|
||||||
SCONAME = "ScoDoc"
|
SCONAME = "ScoDoc"
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user