Import notes toutes évaluations d'un module

This commit is contained in:
Emmanuel Viennet 2024-07-19 16:32:56 +02:00
parent 10623e568b
commit 4de2d63861
9 changed files with 225 additions and 52 deletions

View File

@ -830,7 +830,7 @@ def check_etud_duplicate_code(args, code_name, edit=True):
dest_endpoint = "notes.index_html"
parameters = {}
err_page = f"""<h3><h3>Code étudiant ({code_name}) dupliqué !</h3>
err_page = f"""<h3>Code étudiant ({code_name}) dupliqué !</h3>
<p class="help">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.
</p>
@ -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(

View File

@ -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

View File

@ -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):

View File

@ -478,8 +478,17 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
<a class="stdlink" style="margin-left:2em;" href="{
url_for("notes.moduleimpl_evaluation_renumber",
scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
}">Trier par date</a>
}" title="Ordonner les évaluations par date">Trier par date</a>
"""
bot_table_links = (
top_table_links
+ f"""
<a class="stdlink" style="margin-left:2em;" href="{
url_for("notes.moduleimpl_import_notes",
scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
}" title="Charger toutles les notes via tableur">Importer les notes</a>
"""
)
if nb_evaluations > 0:
H.append(
'<div class="moduleimpl_evaluations_top_links">'
@ -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"""<div class="moduleimpl_evaluations_table_bot">{bot_table_links}</div>"""
)
H.append(
f"""</td></tr>

View File

@ -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(
status_url = (
url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
)
obj_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)
)
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",
)

View File

@ -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;

View File

@ -12,11 +12,27 @@ div.vspace {
{% block app_content %}
<h2>Import de notes dans les évaluations du semestre</h2>
<h2>Import de notes dans les évaluations du
{% if modimpl %}
module
{% else %}
semestre
{% endif %}
</h2>
{% if modimpl %}
<div class="help">
Cette page permet d'importer des notes dans tout ou partie des évaluations du module.
</div>
<div>
Il y a <a class="stdlink" href="{{
url_for('notes.moduleimpl_status', scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id )
}}">{{ evaluations | length }} évaluations définies dans le module {{modimpl.module.code}}</a>.
</div>
{% else %}
<div class="help">
Cette page permet d'importer des notes dans tout ou partie des évaluations du semestre.
</div>
<div>
@ -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</a> définies dans ce semestre.
</div>
{% endif %}
<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 )
url_for( 'notes.moduleimpl_feuille_import_notes', scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id )
if modimpl else
url_for( 'notes.formsemestre_feuille_import_notes', scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id )
}}" id="lnk_feuille_saisie">
obtenir le fichier tableur à remplir
</a>

View File

@ -18,7 +18,13 @@
{% endblock %}
{% block app_content %}
<h2>Import de notes dans les évaluations du semestre</h2>
<h2>Import de notes dans les évaluations du
{% if modimpl %}
module
{% else %}
semestre
{% endif %}
</h2>
<div class="scobox {{ 'success' if ok else 'failure' }}">
<div class="scobox-title">
@ -36,6 +42,23 @@
<div class="scobox">
<ul>
{% if modimpl %}
<li><a class="stdlink"
href="{{url_for('notes.moduleimpl_status',
scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id,
)}}">
{{modimpl.module.type_name()}} {{modimpl.module.code}} {{modimpl.module.titre_str()}}
</a>
</li>
<li><a class="stdlink"
href="{{url_for('notes.evaluation_listenotes',
scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id,
)}}">
Lister les notes de {{modimpl.module.code}}
</a>
</li>
{% endif %}
<li><a class="stdlink"
href="{{url_for('notes.formsemestre_recapcomplet',
scodoc_dept=g.scodoc_dept, formsemestre_id=sco.formsemestre.id,
@ -43,12 +66,14 @@
Tableau de <em>toutes</em> les notes du semestre
</a>
</li>
{% if sco.formsemestre %}
<li><a class="stdlink"
href="{{url_for('notes.formsemestre_import_notes',
scodoc_dept=g.scodoc_dept, formsemestre_id=sco.formsemestre.id)}}">
Importer d'autres notes
</a>
</li>
{% endif %}
</div>
{% endblock %}

View File

@ -1842,19 +1842,57 @@ 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(
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/<int:moduleimpl_id>",
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
)
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 not formsemestre.est_chef_or_diretud():
)
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/<int:formsemestre_id>")
@bp.route("/formsemestre_feuille_import_notes/<int:formsemestre_id>")
@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/<int:moduleimpl_id>")
@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