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.models.ues import UniteEns
from app.scodoc.sco_cache import ResultatsSemestreCache from app.scodoc.sco_cache import ResultatsSemestreCache
from app.scodoc.sco_codes_parcours import UE_SPORT, DEF, DEM 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.sco_exceptions import ScoValueError
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_users from app.scodoc import sco_users
@ -387,7 +388,7 @@ class ResultatsSemestre(ResultatsCache):
# --- TABLEAU RECAP # --- TABLEAU RECAP
def get_table_recap(self, convert_values=False): def get_table_recap(self, convert_values=False, include_evaluations=False):
"""Result: tuple avec """Result: tuple avec
- rows: liste de dicts { column_id : value } - rows: liste de dicts { column_id : value }
- titles: { column_id : title } - titles: { column_id : title }
@ -457,6 +458,11 @@ class ResultatsSemestre(ResultatsCache):
idx = 0 # index de la colonne idx = 0 # index de la colonne
etud = Identite.query.get(etudid) etud = Identite.query.get(etudid)
row = {"etudid": 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 # --- Rang
idx = add_cell( idx = add_cell(
row, "rang", "Rg", self.etud_moy_gen_ranks[etudid], "rang", idx 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_partitions(rows, titles)
self._recap_add_admissions(rows, titles) self._recap_add_admissions(rows, titles)
# tri par rang croissant # tri par rang croissant
rows.sort(key=lambda e: e["_rang_order"]) rows.sort(key=lambda e: e["_rang_order"])
# INFOS POUR FOOTER # INFOS POUR FOOTER
bottom_infos = self._recap_bottom_infos(ues_sans_bonus, modimpl_ids, fmt_note) 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 # Ajoute style "col_empty" aux colonnes de modules vides
for col_id in titles: for col_id in titles:
@ -641,7 +650,9 @@ class ResultatsSemestre(ResultatsCache):
row["moy_gen"] = row.get("moy_gen", "") row["moy_gen"] = row.get("moy_gen", "")
row["_moy_gen_class"] = "col_moy_gen" row["_moy_gen_class"] = "col_moy_gen"
# titre de la ligne: # 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"] = bottom_line.lower() + (
(" " + row["_tr_class"]) if "_tr_class" in row else "" (" " + 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: def _recap_bottom_infos(self, ues, modimpl_ids: set, fmt_note) -> dict:
"""Les informations à mettre en bas de la table: min, max, moy, ECTS""" """Les informations à mettre en bas de la table: min, max, moy, ECTS"""
row_min, row_max, row_moy, row_coef, row_ects = ( row_min, row_max, row_moy, row_coef, row_ects, row_apo = (
{"_tr_class": "bottom_info"}, {"_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"}, {"_tr_class": "bottom_info"},
{"_tr_class": "bottom_info"}, {"_tr_class": "bottom_info"},
{"_tr_class": "bottom_info", "_title": "Code Apogée"},
) )
# --- ECTS # --- ECTS
for ue in ues: for ue in ues:
row_ects[f"moy_ue_{ue.id}"] = ue.ects colid = f"moy_ue_{ue.id}"
row_ects[f"_moy_ue_{ue.id}_class"] = "col_ue" row_ects[colid] = ue.ects
row_ects[f"_{colid}_class"] = "col_ue"
# style cases vides pour borders verticales # style cases vides pour borders verticales
row_coef[f"moy_ue_{ue.id}"] = "" row_coef[colid] = ""
row_coef[f"_moy_ue_{ue.id}_class"] = "col_ue" 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"] = sum([ue.ects or 0 for ue in ues if ue.type != UE_SPORT])
row_ects["_moy_gen_class"] = "col_moy_gen" 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_min["moy_gen"] = fmt_note(self.etud_moy_gen.min())
row_max["moy_gen"] = fmt_note(self.etud_moy_gen.max()) row_max["moy_gen"] = fmt_note(self.etud_moy_gen.max())
row_moy["moy_gen"] = fmt_note(self.etud_moy_gen.mean()) row_moy["moy_gen"] = fmt_note(self.etud_moy_gen.mean())
for ue in ues: for ue in ues:
col_id = f"moy_ue_{ue.id}" colid = f"moy_ue_{ue.id}"
row_min[col_id] = fmt_note(self.etud_moy_ue[ue.id].min()) row_min[colid] = fmt_note(self.etud_moy_ue[ue.id].min())
row_max[col_id] = fmt_note(self.etud_moy_ue[ue.id].max()) row_max[colid] = fmt_note(self.etud_moy_ue[ue.id].max())
row_moy[col_id] = fmt_note(self.etud_moy_ue[ue.id].mean()) row_moy[colid] = fmt_note(self.etud_moy_ue[ue.id].mean())
row_min[f"_{col_id}_class"] = "col_ue" row_min[f"_{colid}_class"] = "col_ue"
row_max[f"_{col_id}_class"] = "col_ue" row_max[f"_{colid}_class"] = "col_ue"
row_moy[f"_{col_id}_class"] = "col_ue" row_moy[f"_{colid}_class"] = "col_ue"
row_apo[colid] = ue.code_apogee or ""
for modimpl in self.formsemestre.modimpls_sorted: for modimpl in self.formsemestre.modimpls_sorted:
if modimpl.id in modimpl_ids: 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: if self.is_apc:
coef = self.modimpl_coefs_df[modimpl.id][ue.id] coef = self.modimpl_coefs_df[modimpl.id][ue.id]
else: else:
coef = modimpl.module.coefficient or 0 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) notes = self.modimpl_notes(modimpl.id, ue.id)
row_min[col_id] = fmt_note(np.nanmin(notes)) row_min[colid] = fmt_note(np.nanmin(notes))
row_max[col_id] = fmt_note(np.nanmax(notes)) row_max[colid] = fmt_note(np.nanmax(notes))
moy = np.nanmean(notes) moy = np.nanmean(notes)
row_moy[col_id] = fmt_note(moy) row_moy[colid] = fmt_note(moy)
if np.isnan(moy): if np.isnan(moy):
# aucune note dans ce module # 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 return { # { key : row } avec key = min, max, moy, coef
"min": row_min, "min": row_min,
@ -710,6 +726,7 @@ class ResultatsSemestre(ResultatsCache):
"moy": row_moy, "moy": row_moy,
"coef": row_coef, "coef": row_coef,
"ects": row_ects, "ects": row_ects,
"apo": row_apo,
} }
def _recap_etud_groups_infos(self, etudid: int, row: dict, titles: dict): 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}"] = gr_name
row[f"_{cid}_class"] = klass row[f"_{cid}_class"] = klass
first_partition = False 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): def formsemestre_delete(formsemestre_id):
"""Delete a formsemestre (affiche avertissements)""" """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] F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
H = [ H = [
html_sco_header.html_sem_header("Suppression du semestre"), 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">' '<select name="tabformat" onchange="document.f.submit()" class="noprint">'
) )
for (format, label) in ( for (format, label) in (
("html", "HTML"), ("html", "Tableau"),
("xls", "Fichier tableur (Excel)"), ("evals", "Avec toutes les évaluations"),
("xlsall", "Fichier tableur avec toutes les évals"), ("xml", "Bulletins XML (obsolète)"),
("csv", "Fichier tableur (CSV)"), ("json", "Bulletins JSON"),
("xml", "Fichier XML"),
("json", "JSON"),
): ):
if format == tabformat: if format == tabformat:
selected = " selected" selected = " selected"
@ -149,7 +147,6 @@ def formsemestre_recapcomplet(
href="{url_for('notes.formsemestre_bulletins_pdf', href="{url_for('notes.formsemestre_bulletins_pdf',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)}"> scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)}">
ici avoir le classeur papier</a>) 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.""" """Calcule et renvoie le tableau récapitulatif."""
formsemestre = FormSemestre.query.get_or_404(formsemestre_id) 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) 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: else:
data, filename, format = make_formsemestre_recapcomplet( data, filename, format = make_formsemestre_recapcomplet(
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
@ -239,7 +238,7 @@ def do_formsemestre_recapcomplet(
force_publishing=force_publishing, force_publishing=force_publishing,
) )
# --- # ---
if format == "xml" or format == "html": if format == "xml" or format == "html" or format == "evals":
return data return data
elif format == "csv": elif format == "csv":
return scu.send_file(data, filename=filename, mime=scu.CSV_MIMETYPE) 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 js, filename=filename, suffix=scu.JSON_SUFFIX, mime=scu.JSON_MIMETYPE
) )
else: else:
raise ValueError("unknown format %s" % format) raise ValueError(f"unknown format {format}")
def make_formsemestre_recapcomplet( def make_formsemestre_recapcomplet(
formsemestre_id=None, 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) hidemodules=False, # ne pas montrer les modules (ignoré en XML)
hidebac=False, # pas de colonne Bac (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) 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( def gen_formsemestre_recapcomplet_html(
formsemestre: FormSemestre, res: NotesTableCompat formsemestre: FormSemestre, res: NotesTableCompat, include_evaluations=False
): ):
"""Construit table recap pour le BUT """Construit table recap pour le BUT
Return: data, filename 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: if not rows:
return ( return (
'<div class="table_recap"><div class="message">aucun étudiant !</div></div>', '<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 = [ 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 # header
H.append( H.append(
@ -1068,7 +1074,4 @@ def gen_formsemestre_recapcomplet_html(
</div> </div>
""" """
) )
return ( return ("".join(H), filename) # suffix ?
"".join(H),
f'recap-{formsemestre.titre_num().replace(" ", "_")}-{time.strftime("%d-%m-%Y")}',
) # suffix ?

View File

@ -3704,3 +3704,28 @@ table.table_recap tr.def td {
color: rgb(121, 74, 74); color: rgb(121, 74, 74);
font-style: italic; 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, colReorder: true,
"columnDefs": [ "columnDefs": [
{ {
// cache le détail de l'identité, les groupes, les colonnes admission et les vides // cache les codes, le détail de l'identité, les groupes, les colonnes admission et les vides
"targets": ["identite_detail", "partition_aux", "admission", "col_empty"], targets: ["codes", "identite_detail", "partition_aux", "admission", "col_empty"],
"visible": false, 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', 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', extend: 'collection',
text: 'Colonnes affichées', text: 'Colonnes affichées',