diff --git a/app/models/etudiants.py b/app/models/etudiants.py
index 61ca8e5b8..e64bbbb87 100644
--- a/app/models/etudiants.py
+++ b/app/models/etudiants.py
@@ -830,7 +830,7 @@ def check_etud_duplicate_code(args, code_name, edit=True):
dest_endpoint = "notes.index_html"
parameters = {}
- err_page = f"""
Le {code_name} {args[code_name]} est déjà utilisé: un seul étudiant peut avoir
ce code. Vérifier votre valeur ou supprimer l'autre étudiant avec cette valeur.
@@ -845,7 +845,7 @@ def check_etud_duplicate_code(args, code_name, edit=True):
log(f"*** error: code {code_name} duplique: {args[code_name]}")
- raise ScoGenError(err_page)
+ raise ScoGenError(err_page, safe=True)
def make_etud_args(
diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py
index da6050936..1ef1df495 100644
--- a/app/scodoc/sco_excel.py
+++ b/app/scodoc/sco_excel.py
@@ -508,24 +508,26 @@ def excel_simple_table(
return ws.generate()
-def excel_bytes_to_list(bytes_content):
+def excel_bytes_to_list(bytes_content) -> tuple[list, list[list]]:
+ "Lecture d'un flux xlsx"
try:
filelike = io.BytesIO(bytes_content)
+ return _excel_to_list(filelike)
except Exception as exc:
raise ScoValueError(
- """Le fichier xlsx attendu n'est pas lisible !
+ """Le fichier xlsx attendu n'est pas lisible ! (1)
Peut-être avez-vous fourni un fichier au mauvais format (txt, xls, ..)
"""
) from exc
- return _excel_to_list(filelike)
-def excel_file_to_list(filename):
+def excel_file_to_list(filelike) -> tuple[list, list[list]]:
+ "Lecture d'un flux xlsx"
try:
- return _excel_to_list(filename)
+ return _excel_to_list(filelike)
except Exception as exc:
raise ScoValueError(
- """Le fichier xlsx attendu n'est pas lisible !
+ """Le fichier xlsx attendu n'est pas lisible ! (2)
Peut-être avez-vous fourni un fichier au mauvais format (txt, xls, ...)
"""
) from exc
@@ -611,7 +613,7 @@ def excel_workbook_to_list(filelike):
workbook = _open_workbook(filelike)
except Exception as exc:
raise ScoValueError(
- """Le fichier xlsx attendu n'est pas lisible !
+ """Le fichier xlsx attendu n'est pas lisible ! (3)
Peut-être avez-vous fourni un fichier au mauvais format (txt, xls, ...)
"""
) from exc
diff --git a/app/scodoc/sco_exceptions.py b/app/scodoc/sco_exceptions.py
index 53b1711c2..741ad9fdb 100644
--- a/app/scodoc/sco_exceptions.py
+++ b/app/scodoc/sco_exceptions.py
@@ -200,11 +200,11 @@ class ScoNoReferentielCompetences(ScoValueError):
super().__init__(msg)
-class ScoGenError(ScoException):
+class ScoGenError(ScoValueError):
"exception avec affichage d'une page explicative ad-hoc"
- def __init__(self, msg=""):
- super().__init__(msg)
+ def __init__(self, msg="", safe=False):
+ super().__init__(msg, safe=safe)
class AccessDenied(ScoGenError):
diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py
index 514321f9f..8fe8eb3f4 100644
--- a/app/scodoc/sco_moduleimpl_status.py
+++ b/app/scodoc/sco_moduleimpl_status.py
@@ -478,8 +478,17 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
'
@@ -514,7 +523,9 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
if sem_locked:
H.append(f"""{scu.icontag("lock32_img")} semestre verrouillé""")
elif can_edit_evals:
- H.append(top_table_links)
+ H.append(
+ f"""
{bot_table_links}
"""
+ )
H.append(
f"""
diff --git a/app/scodoc/sco_saisie_excel.py b/app/scodoc/sco_saisie_excel.py
index e5af4c479..ee9e357d9 100644
--- a/app/scodoc/sco_saisie_excel.py
+++ b/app/scodoc/sco_saisie_excel.py
@@ -34,7 +34,7 @@ saisie_notes_tableur (formulaire)
## Notes d'un semestre
formsemestre_import_notes (formulaire, import_notes.j2)
- -> feuille_import_notes (génération de l'excel)
+ -> formsemestre_feuille_import_notes (génération de l'excel)
-> formsemestre_import_notes
@@ -48,7 +48,7 @@ from openpyxl.styles.numbers import FORMAT_GENERAL
from flask import g, render_template, request, url_for
from flask_login import current_user
-from app.models import Evaluation, FormSemestre, Identite, ScolarNews
+from app.models import Evaluation, FormSemestre, Identite, ModuleImpl, ScolarNews
from app.scodoc.sco_excel import COLORS, ScoExcelSheet
from app.scodoc import (
html_sco_header,
@@ -61,7 +61,7 @@ from app.scodoc import (
sco_saisie_notes,
sco_users,
)
-from app.scodoc.sco_exceptions import AccessDenied, InvalidNoteValue
+from app.scodoc.sco_exceptions import AccessDenied, InvalidNoteValue, ScoValueError
import app.scodoc.sco_utils as scu
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.views import ScoData
@@ -435,12 +435,27 @@ def feuille_saisie_notes(
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,
+def excel_feuille_import(
+ formsemestre: FormSemestre | None = None, modimpl: ModuleImpl | None = None
+) -> AnyStr:
+ """Génère feuille pour import toutes notes dans ce semestre ou ce module,
avec une colonne par évaluation.
Return excel data
"""
- evaluations = formsemestre.get_evaluations()
+ if not (formsemestre or modimpl):
+ raise ScoValueError("excel_feuille_import: missing argument")
+ evaluations = (
+ formsemestre.get_evaluations() if formsemestre else modimpl.evaluations.all()
+ )
+ if formsemestre is None:
+ if not evaluations:
+ raise ScoValueError(
+ "pas d'évaluations dans ce module",
+ dest_url=url_for(
+ "notes.moduleimpl_status, scodoc_dept=g.scodoc_dept, moduleimpl_id=modipl.id"
+ ),
+ )
+ formsemestre = evaluations[0].moduleimpl.formsemestre
etudiants = formsemestre.get_inscrits(include_demdef=True, order=True)
rows = [{"etud": etud} for etud in etudiants]
# Liste les étudiants et leur note à chaque évaluation
@@ -543,14 +558,16 @@ def generate_excel_import_notes(
def do_evaluations_upload_xls(
- notefile,
+ notefile="",
comment: str = "",
evaluation: Evaluation | None = None,
formsemestre: FormSemestre | None = None,
+ modimpl: ModuleImpl | None = None,
) -> tuple[bool, str]:
"""
Soumission d'un fichier XLS (evaluation_id, notefile)
soit dans le formsemestre (import multi-eval)
+ soit dans toules les évaluations du modimpl
soit dans une seule évaluation
return:
ok: bool
@@ -563,7 +580,11 @@ def do_evaluations_upload_xls(
# Lecture des évaluations ids
row_title_idx, evaluations, evaluations_col_idx = _get_sheet_evaluations(
- rows, evaluation=evaluation, formsemestre=formsemestre, diag=diag
+ rows,
+ evaluation=evaluation,
+ formsemestre=formsemestre,
+ modimpl=modimpl,
+ diag=diag,
)
# Vérification des permissions (admin, resp. formation, responsable_id, ens)
@@ -591,12 +612,26 @@ def do_evaluations_upload_xls(
modules_str = ", ".join(
[evaluation.moduleimpl.module.code for evaluation in evaluations]
)
- status_url = url_for(
- "notes.formsemestre_status",
- scodoc_dept=g.scodoc_dept,
- formsemestre_id=formsemestre.id,
+ status_url = (
+ url_for(
+ "notes.formsemestre_status",
+ scodoc_dept=g.scodoc_dept,
+ formsemestre_id=formsemestre.id,
+ )
+ if formsemestre
+ else (
+ url_for(
+ "notes.moduleimpl_status",
+ scodoc_dept=g.scodoc_dept,
+ moduleimpl_id=modimpl.id,
+ )
+ if modimpl
+ else ""
+ )
+ )
+ obj_id = (
+ formsemestre.id if formsemestre else (modimpl.id if modimpl else None)
)
- obj_id = formsemestre.id
else:
modules_str = (
evaluation.moduleimpl.module.titre or evaluation.moduleimpl.module.code
@@ -862,6 +897,7 @@ def _get_sheet_evaluations(
rows: list[list[str]],
evaluation: Evaluation | None = None,
formsemestre: FormSemestre | None = None,
+ modimpl: ModuleImpl | None = None,
diag: list[str] = None,
) -> tuple[int, list[Evaluation], dict[int, int]]:
"""
@@ -869,7 +905,8 @@ def _get_sheet_evaluations(
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.
+ modimpl (optionel): le moduleimpl dans lequel sont les évaluations à remplir
+ formsemestre ou evaluation ou modimpl doivent être indiqués.
Résultat:
row_title_idx: l'indice (à partir de 0) de la ligne TITRE (après laquelle commencent les notes)
@@ -896,7 +933,15 @@ def _get_sheet_evaluations(
if evaluation is None:
diag.append(f"""Erreur: l'évaluation {evaluation_id} n'existe pas""")
raise InvalidNoteValue()
- if evaluation.moduleimpl.formsemestre_id != formsemestre.id:
+ if (
+ formsemestre
+ and evaluation.moduleimpl.formsemestre_id != formsemestre.id
+ ):
+ diag.append(
+ f"""Erreur: l'évaluation {evaluation_id} n'existe pas dans ce semestre"""
+ )
+ raise InvalidNoteValue()
+ if modimpl and evaluation.moduleimpl.id != modimpl.id:
diag.append(
f"""Erreur: l'évaluation {evaluation_id} n'existe pas dans ce semestre"""
)
@@ -1014,7 +1059,7 @@ def saisie_notes_tableur(evaluation_id: int, group_ids=()):
args = scu.get_request_args()
evaluation = Evaluation.get_evaluation(args["evaluation_id"])
ok, diagnostic_msg = do_evaluations_upload_xls(
- args["notefile"], evaluation=evaluation, comment=args["comment"]
+ notefile=args["notefile"], evaluation=evaluation, comment=args["comment"]
)
if ok:
H.append(
@@ -1124,16 +1169,23 @@ def saisie_notes_tableur(evaluation_id: int, group_ids=()):
return "\n".join(H)
-def formsemestre_import_notes(formsemestre: FormSemestre, notefile, comment: str):
- """Importation de notes dans plusieurs évaluations du semestre"""
+def formsemestre_import_notes(
+ formsemestre: FormSemestre | None = None,
+ modimpl: ModuleImpl | None = None,
+ notefile="",
+ comment: str = "",
+):
+ """Importation de notes dans plusieurs évaluations
+ du formsemestre ou du modimpl"""
ok, diagnostic_msg = do_evaluations_upload_xls(
- notefile, formsemestre=formsemestre, comment=comment
+ notefile=notefile, formsemestre=formsemestre, modimpl=modimpl, comment=comment
)
return render_template(
"formsemestre/import_notes_after.j2",
comment=comment,
ok=ok,
diagnostic_msg=diagnostic_msg,
- sco=ScoData(formsemestre=formsemestre),
+ modimpl=modimpl,
+ sco=ScoData(formsemestre=formsemestre or modimpl.formsemestre),
title="Importation des notes",
)
diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css
index 015c98ec9..9a6e07f00 100644
--- a/app/static/css/scodoc.css
+++ b/app/static/css/scodoc.css
@@ -2168,6 +2168,11 @@ span.moduleimpl_abs_link {
margin-bottom: 3px;
}
+.moduleimpl_evaluations_table_bot {
+ margin-top: 12px;
+ margin-bottom: 12px;
+}
+
table.moduleimpl_evaluations {
width: 100%;
border-spacing: 0px;
diff --git a/app/templates/formsemestre/import_notes.j2 b/app/templates/formsemestre/import_notes.j2
index 8fd757226..0c0f05395 100644
--- a/app/templates/formsemestre/import_notes.j2
+++ b/app/templates/formsemestre/import_notes.j2
@@ -12,11 +12,27 @@ div.vspace {
{% block app_content %}
-
Import de notes dans les évaluations du semestre
+
Import de notes dans les évaluations du
+{% if modimpl %}
+ module
+{% else %}
+ semestre
+{% endif %}
+
+{% if modimpl %}
+
+Cette page permet d'importer des notes dans tout ou partie des évaluations du module.
+
+
+
+{% else %}
Cette page permet d'importer des notes dans tout ou partie des évaluations du semestre.
-
@@ -24,12 +40,15 @@ Cette page permet d'importer des notes dans tout ou partie des évaluations du s
url_for('notes.evaluations_recap', scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id )
}}">{{ evaluations | length }} évaluations définies dans ce semestre.
+{% endif %}
Étape 1 :
-
obtenir le fichier tableur à remplir
diff --git a/app/templates/formsemestre/import_notes_after.j2 b/app/templates/formsemestre/import_notes_after.j2
index 5f7222c81..8200c35c0 100644
--- a/app/templates/formsemestre/import_notes_after.j2
+++ b/app/templates/formsemestre/import_notes_after.j2
@@ -18,7 +18,13 @@
{% endblock %}
{% block app_content %}
-
Import de notes dans les évaluations du semestre
+Import de notes dans les évaluations du
+{% if modimpl %}
+ module
+{% else %}
+ semestre
+{% endif %}
+
@@ -36,6 +42,23 @@
{% endblock %}
diff --git a/app/views/notes.py b/app/views/notes.py
index 95c071a76..476535c37 100644
--- a/app/views/notes.py
+++ b/app/views/notes.py
@@ -1842,19 +1842,57 @@ sco_publish(
@bp.route("/formsemestre_import_notes/
", 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,
+def formsemestre_import_notes(formsemestre_id: int | None = None):
+ "Import via excel des notes de toutes les évals d'un semestre."
+ return _formsemestre_or_modimpl_import_notes(formsemestre_id=formsemestre_id)
+
+
+@bp.route(
+ "/moduleimpl_import_notes/",
+ methods=["GET", "POST"],
+)
+@scodoc
+@permission_required(Permission.ScoView) # controle contextuel
+def moduleimpl_import_notes(moduleimpl_id: int | None = None):
+ "Import via excel des notes de toutes les évals d'un module."
+ return _formsemestre_or_modimpl_import_notes(moduleimpl_id=moduleimpl_id)
+
+
+def _formsemestre_or_modimpl_import_notes(
+ formsemestre_id: int | None = None, moduleimpl_id: int | None = None
+):
+ """Import via excel des notes de toutes les évals d'un semestre.
+ Ou, si moduleimpl_import_notes, toutes les évals de ce module.
+ """
+ formsemestre = (
+ FormSemestre.get_formsemestre(formsemestre_id)
+ if formsemestre_id is not None
+ else None
)
- if not formsemestre.est_chef_or_diretud():
+ modimpl = (
+ ModuleImpl.get_modimpl(moduleimpl_id) if moduleimpl_id is not None else None
+ )
+ if not (formsemestre or modimpl):
+ raise ScoValueError("paramètre manquant")
+ dest_url = (
+ url_for(
+ "notes.moduleimpl_status",
+ scodoc_dept=g.scodoc_dept,
+ moduleimpl_id=modimpl.id,
+ )
+ if modimpl
+ else url_for(
+ "notes.formsemestre_status",
+ scodoc_dept=g.scodoc_dept,
+ formsemestre_id=formsemestre.id,
+ )
+ )
+ if formsemestre and not formsemestre.est_chef_or_diretud():
+ raise ScoPermissionDenied("opération non autorisée", dest_url=dest_url)
+ if modimpl and not modimpl.can_edit_notes(current_user):
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=[
@@ -1872,30 +1910,51 @@ def formsemestre_import_notes(formsemestre_id: int):
comment = form.comment.data
#
return sco_saisie_excel.formsemestre_import_notes(
- formsemestre, notefile, comment
+ formsemestre=formsemestre,
+ modimpl=modimpl,
+ notefile=notefile,
+ comment=comment,
)
return render_template(
"formsemestre/import_notes.j2",
- evaluations=formsemestre.get_evaluations(),
+ evaluations=(
+ formsemestre.get_evaluations()
+ if formsemestre
+ else modimpl.evaluations.all()
+ ),
form=form,
formsemestre=formsemestre,
+ modimpl=modimpl,
title="Importation des notes",
sco=ScoData(formsemestre=formsemestre),
)
-@bp.route("/feuille_import_notes/")
+@bp.route("/formsemestre_feuille_import_notes/")
@scodoc
@permission_required(Permission.ScoView)
-def feuille_import_notes(formsemestre_id: int):
+def formsemestre_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)
+ xls = sco_saisie_excel.excel_feuille_import(formsemestre=formsemestre)
filename = scu.sanitize_filename(formsemestre.titre_annee())
return scu.send_file(xls, filename, scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE)
+@bp.route("/moduleimpl_feuille_import_notes/")
+@scodoc
+@permission_required(Permission.ScoView)
+def moduleimpl_feuille_import_notes(moduleimpl_id: int):
+ """Feuille excel pour importer les notes de toutes les évaluations du modimpl"""
+ modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
+ xls = sco_saisie_excel.excel_feuille_import(modimpl=modimpl)
+ filename = scu.sanitize_filename(
+ f"{modimpl.module.code} {modimpl.formsemestre.annee_scolaire_str()}"
+ )
+ return scu.send_file(xls, filename, scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE)
+
+
# --- Bulletins
@bp.route("/formsemestre_bulletins_pdf")
@scodoc