Export excel depuis table recap: closes #351. Export codes Apo. Export notes évaluations.

This commit is contained in:
Emmanuel Viennet 2022-04-04 09:35:52 +02:00
parent 841ae1c7ab
commit 6b49c8472d
5 changed files with 160 additions and 46 deletions

View File

@ -23,6 +23,7 @@ from app.models import ModuleImpl, ModuleImplInscription
from app.models.ues import UniteEns
from app.scodoc.sco_cache import ResultatsSemestreCache
from app.scodoc.sco_codes_parcours import UE_SPORT, DEF, DEM
from app.scodoc import sco_evaluation_db
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_groups
from app.scodoc import sco_users
@ -387,7 +388,7 @@ class ResultatsSemestre(ResultatsCache):
# --- TABLEAU RECAP
def get_table_recap(self, convert_values=False):
def get_table_recap(self, convert_values=False, include_evaluations=False):
"""Result: tuple avec
- rows: liste de dicts { column_id : value }
- titles: { column_id : title }
@ -457,6 +458,11 @@ class ResultatsSemestre(ResultatsCache):
idx = 0 # index de la colonne
etud = Identite.query.get(etudid)
row = {"etudid": etudid}
# --- Codes (seront cachés, mais exportés en excel)
idx = add_cell(row, "etudid", "etudid", etudid, "codes", idx)
idx = add_cell(
row, "code_nip", "code_nip", etud.code_nip or "", "codes", idx
)
# --- Rang
idx = add_cell(
row, "rang", "Rg", self.etud_moy_gen_ranks[etudid], "rang", idx
@ -618,11 +624,14 @@ class ResultatsSemestre(ResultatsCache):
self._recap_add_partitions(rows, titles)
self._recap_add_admissions(rows, titles)
# tri par rang croissant
rows.sort(key=lambda e: e["_rang_order"])
# INFOS POUR FOOTER
bottom_infos = self._recap_bottom_infos(ues_sans_bonus, modimpl_ids, fmt_note)
if include_evaluations:
self._recap_add_evaluations(rows, titles, bottom_infos)
# Ajoute style "col_empty" aux colonnes de modules vides
for col_id in titles:
@ -641,7 +650,9 @@ class ResultatsSemestre(ResultatsCache):
row["moy_gen"] = row.get("moy_gen", "")
row["_moy_gen_class"] = "col_moy_gen"
# titre de la ligne:
row["prenom"] = row["nom_short"] = bottom_line.capitalize()
row["prenom"] = row["nom_short"] = (
row.get(f"_title", "") or bottom_line.capitalize()
)
row["_tr_class"] = bottom_line.lower() + (
(" " + row["_tr_class"]) if "_tr_class" in row else ""
)
@ -656,53 +667,58 @@ class ResultatsSemestre(ResultatsCache):
def _recap_bottom_infos(self, ues, modimpl_ids: set, fmt_note) -> dict:
"""Les informations à mettre en bas de la table: min, max, moy, ECTS"""
row_min, row_max, row_moy, row_coef, row_ects = (
{"_tr_class": "bottom_info"},
row_min, row_max, row_moy, row_coef, row_ects, row_apo = (
{"_tr_class": "bottom_info", "_title": "Min."},
{"_tr_class": "bottom_info"},
{"_tr_class": "bottom_info"},
{"_tr_class": "bottom_info"},
{"_tr_class": "bottom_info"},
{"_tr_class": "bottom_info", "_title": "Code Apogée"},
)
# --- ECTS
for ue in ues:
row_ects[f"moy_ue_{ue.id}"] = ue.ects
row_ects[f"_moy_ue_{ue.id}_class"] = "col_ue"
colid = f"moy_ue_{ue.id}"
row_ects[colid] = ue.ects
row_ects[f"_{colid}_class"] = "col_ue"
# style cases vides pour borders verticales
row_coef[f"moy_ue_{ue.id}"] = ""
row_coef[f"_moy_ue_{ue.id}_class"] = "col_ue"
row_coef[colid] = ""
row_coef[f"_{colid}_class"] = "col_ue"
# row_apo[colid] = ue.code_apogee or ""
row_ects["moy_gen"] = sum([ue.ects or 0 for ue in ues if ue.type != UE_SPORT])
row_ects["_moy_gen_class"] = "col_moy_gen"
# --- MIN, MAX, MOY
# --- MIN, MAX, MOY, APO
row_min["moy_gen"] = fmt_note(self.etud_moy_gen.min())
row_max["moy_gen"] = fmt_note(self.etud_moy_gen.max())
row_moy["moy_gen"] = fmt_note(self.etud_moy_gen.mean())
for ue in ues:
col_id = f"moy_ue_{ue.id}"
row_min[col_id] = fmt_note(self.etud_moy_ue[ue.id].min())
row_max[col_id] = fmt_note(self.etud_moy_ue[ue.id].max())
row_moy[col_id] = fmt_note(self.etud_moy_ue[ue.id].mean())
row_min[f"_{col_id}_class"] = "col_ue"
row_max[f"_{col_id}_class"] = "col_ue"
row_moy[f"_{col_id}_class"] = "col_ue"
colid = f"moy_ue_{ue.id}"
row_min[colid] = fmt_note(self.etud_moy_ue[ue.id].min())
row_max[colid] = fmt_note(self.etud_moy_ue[ue.id].max())
row_moy[colid] = fmt_note(self.etud_moy_ue[ue.id].mean())
row_min[f"_{colid}_class"] = "col_ue"
row_max[f"_{colid}_class"] = "col_ue"
row_moy[f"_{colid}_class"] = "col_ue"
row_apo[colid] = ue.code_apogee or ""
for modimpl in self.formsemestre.modimpls_sorted:
if modimpl.id in modimpl_ids:
col_id = f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}"
colid = f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}"
if self.is_apc:
coef = self.modimpl_coefs_df[modimpl.id][ue.id]
else:
coef = modimpl.module.coefficient or 0
row_coef[col_id] = fmt_note(coef)
row_coef[colid] = fmt_note(coef)
notes = self.modimpl_notes(modimpl.id, ue.id)
row_min[col_id] = fmt_note(np.nanmin(notes))
row_max[col_id] = fmt_note(np.nanmax(notes))
row_min[colid] = fmt_note(np.nanmin(notes))
row_max[colid] = fmt_note(np.nanmax(notes))
moy = np.nanmean(notes)
row_moy[col_id] = fmt_note(moy)
row_moy[colid] = fmt_note(moy)
if np.isnan(moy):
# aucune note dans ce module
row_moy[f"_{col_id}_class"] = "col_empty"
row_moy[f"_{colid}_class"] = "col_empty"
row_apo[colid] = modimpl.module.code_apogee or ""
return { # { key : row } avec key = min, max, moy, coef
"min": row_min,
@ -710,6 +726,7 @@ class ResultatsSemestre(ResultatsCache):
"moy": row_moy,
"coef": row_coef,
"ects": row_ects,
"apo": row_apo,
}
def _recap_etud_groups_infos(self, etudid: int, row: dict, titles: dict):
@ -803,3 +820,48 @@ class ResultatsSemestre(ResultatsCache):
row[f"{cid}"] = gr_name
row[f"_{cid}_class"] = klass
first_partition = False
def _recap_add_evaluations(
self, rows: list[dict], titles: dict, bottom_infos: dict
):
"""Ajoute les colonnes avec les notes aux évaluations
rows est une liste de dict avec une clé "etudid"
Les colonnes ont la classe css "evaluation"
"""
# nouvelle ligne pour description évaluations:
bottom_infos["descr_evaluation"] = {
"_tr_class": "bottom_info",
"_title": "Description évaluation",
}
first = True
for modimpl in self.formsemestre.modimpls_sorted:
evals = self.modimpls_results[modimpl.id].get_evaluations_completes(modimpl)
eval_index = len(evals) - 1
inscrits = {i.etudid for i in modimpl.inscriptions}
klass = "evaluation first" if first else "evaluation"
first = False
for i, e in enumerate(evals):
cid = f"eval_{e.id}"
titles[
cid
] = f'{modimpl.module.code} {eval_index} {e.jour.isoformat() if e.jour else ""}'
titles[f"_{cid}_class"] = klass
titles[f"_{cid}_col_order"] = 9000 + i # à droite
eval_index -= 1
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(
e.evaluation_id
)
for row in rows:
etudid = row["etudid"]
if etudid in inscrits:
if etudid in notes_db:
val = notes_db[etudid]["value"]
else:
# Note manquante mais prise en compte immédiate: affiche ATT
val = scu.NOTES_ATTENTE
row[cid] = scu.fmt_note(val)
row[f"_{cid}_class"] = klass
bottom_infos["coef"][cid] = e.coefficient
bottom_infos["min"][cid] = "0"
bottom_infos["max"][cid] = scu.fmt_note(e.note_max)
bottom_infos["descr_evaluation"][cid] = e.description or ""

View File

@ -1314,7 +1314,7 @@ def _reassociate_moduleimpls(cnx, formsemestre_id, ues_old2new, modules_old2new)
def formsemestre_delete(formsemestre_id):
"""Delete a formsemestre (affiche avertissements)"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
H = [
html_sco_header.html_sem_header("Suppression du semestre"),

View File

@ -130,12 +130,10 @@ def formsemestre_recapcomplet(
'<select name="tabformat" onchange="document.f.submit()" class="noprint">'
)
for (format, label) in (
("html", "HTML"),
("xls", "Fichier tableur (Excel)"),
("xlsall", "Fichier tableur avec toutes les évals"),
("csv", "Fichier tableur (CSV)"),
("xml", "Fichier XML"),
("json", "JSON"),
("html", "Tableau"),
("evals", "Avec toutes les évaluations"),
("xml", "Bulletins XML (obsolète)"),
("json", "Bulletins JSON"),
):
if format == tabformat:
selected = " selected"
@ -149,7 +147,6 @@ def formsemestre_recapcomplet(
href="{url_for('notes.formsemestre_bulletins_pdf',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)}">
ici avoir le classeur papier</a>)
<div class="warning">Nouvelle version: export excel inachevés. Merci de signaler les problèmes.</div>
"""
)
@ -221,9 +218,11 @@ def do_formsemestre_recapcomplet(
):
"""Calcule et renvoie le tableau récapitulatif."""
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if format == "html" and not modejury:
if (format == "html" or format == "evals") and not modejury:
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
data, filename = gen_formsemestre_recapcomplet_html(formsemestre, res)
data, filename = gen_formsemestre_recapcomplet_html(
formsemestre, res, include_evaluations=(format == "evals")
)
else:
data, filename, format = make_formsemestre_recapcomplet(
formsemestre_id=formsemestre_id,
@ -239,7 +238,7 @@ def do_formsemestre_recapcomplet(
force_publishing=force_publishing,
)
# ---
if format == "xml" or format == "html":
if format == "xml" or format == "html" or format == "evals":
return data
elif format == "csv":
return scu.send_file(data, filename=filename, mime=scu.CSV_MIMETYPE)
@ -251,12 +250,12 @@ def do_formsemestre_recapcomplet(
js, filename=filename, suffix=scu.JSON_SUFFIX, mime=scu.JSON_MIMETYPE
)
else:
raise ValueError("unknown format %s" % format)
raise ValueError(f"unknown format {format}")
def make_formsemestre_recapcomplet(
formsemestre_id=None,
format="html", # html, xml, xls, xlsall, json
format="html", # html, evals, xml, json
hidemodules=False, # ne pas montrer les modules (ignoré en XML)
hidebac=False, # pas de colonne Bac (ignoré en XML)
xml_nodate=False, # format XML sans dates (sert pour debug cache: comparaison de XML)
@ -1029,19 +1028,26 @@ def _gen_row(keys: list[str], row, elt="td"):
def gen_formsemestre_recapcomplet_html(
formsemestre: FormSemestre, res: NotesTableCompat
formsemestre: FormSemestre, res: NotesTableCompat, include_evaluations=False
):
"""Construit table recap pour le BUT
Return: data, filename
"""
rows, footer_rows, titles, column_ids = res.get_table_recap(convert_values=True)
rows, footer_rows, titles, column_ids = res.get_table_recap(
convert_values=True, include_evaluations=include_evaluations
)
if not rows:
return (
'<div class="table_recap"><div class="message">aucun étudiant !</div></div>',
"",
)
filename = scu.sanitize_filename(
f"""recap-{formsemestre.titre_num()}-{time.strftime("%Y-%m-%d")}"""
)
H = [
f"""<div class="table_recap"><table class="table_recap {'apc' if formsemestre.formation.is_apc() else 'classic'}">"""
f"""<div class="table_recap"><table class="table_recap {
'apc' if formsemestre.formation.is_apc() else 'classic'}"
data-filename="{filename}">"""
]
# header
H.append(
@ -1068,7 +1074,4 @@ def gen_formsemestre_recapcomplet_html(
</div>
"""
)
return (
"".join(H),
f'recap-{formsemestre.titre_num().replace(" ", "_")}-{time.strftime("%d-%m-%Y")}',
) # suffix ?
return ("".join(H), filename) # suffix ?

View File

@ -3704,3 +3704,28 @@ table.table_recap tr.def td {
color: rgb(121, 74, 74);
font-style: italic;
}
table.table_recap td.evaluation,
table.table_recap tr.descr_evaluation {
font-size: 90%;
color: rgb(4, 16, 159);
}
table.table_recap tr.descr_evaluation {
vertical-align: top;
}
table.table_recap tr.apo {
font-size: 75%;
font-family: monospace;
}
table.table_recap tr.apo td {
border: 1px solid gray;
background-color: #d8f5fe;
}
table.table_recap td.evaluation.first,
table.table_recap th.evaluation.first {
border-left: 2px solid rgb(4, 16, 159);
}

View File

@ -90,13 +90,37 @@ $(function () {
colReorder: true,
"columnDefs": [
{
// cache le détail de l'identité, les groupes, les colonnes admission et les vides
"targets": ["identite_detail", "partition_aux", "admission", "col_empty"],
"visible": false,
// cache les codes, le détail de l'identité, les groupes, les colonnes admission et les vides
targets: ["codes", "identite_detail", "partition_aux", "admission", "col_empty"],
visible: false,
},
{
// Elimine les 0 à gauche pour les exports excel et les "copy"
targets: ["col_mod", "col_moy_gen", "col_ue"],
render: function (data, type, row) {
return type === 'export' ? data.replace(/0(\d\..*)/, '$1') : data;
}
},
{
// Elimine les décorations (fleches bonus/malus) pour les exports
targets: ["col_ue_bonus", "col_malus"],
render: function (data, type, row) {
return type === 'export' ? data.replace(/.*(\d\d\.\d\d)/, '$1').replace(/0(\d\..*)/, '$1') : data;
}
},
],
dom: 'Bfrtip',
buttons: ['copy', 'excel', 'pdf',
buttons: [
{
extend: 'copyHtml5',
text: 'Copier',
exportOptions: { orthogonal: 'export' }
},
{
extend: 'excelHtml5',
exportOptions: { orthogonal: 'export' },
title: document.querySelector('table.table_recap').dataset.filename
},
{
extend: 'collection',
text: 'Colonnes affichées',