forked from ScoDoc/ScoDoc
Export excel depuis table recap: closes #351. Export codes Apo. Export notes évaluations.
This commit is contained in:
parent
841ae1c7ab
commit
6b49c8472d
@ -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 ""
|
||||
|
@ -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"),
|
||||
|
@ -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 ?
|
||||
|
@ -3703,4 +3703,29 @@ table.table_recap tr.dem td {
|
||||
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);
|
||||
}
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user