diff --git a/app/comp/res_common.py b/app/comp/res_common.py index ed2232713ec074d12623ed4e137b1c34c53a7efc..112cc6493c1ed7ff49550bc9d7f8d1525213b1c6 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -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 "" diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index 685707b9937d14e04d449e9827b0401692756350..7d903f6cfeccddf6edfa44744b5940b38ccb4573 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -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"), diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py index a00e7df39fd25d11b54a0a33b95e61ac72444888..f7b365ed01b6027e16f57de487bb1af45ac39a67 100644 --- a/app/scodoc/sco_recapcomplet.py +++ b/app/scodoc/sco_recapcomplet.py @@ -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 ? diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index f393ac46356ad96c965f161de0b06aa07d2e5ea0..88cc9ba69b5e75c28df17bf816bdf657d8f17614 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -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); } \ No newline at end of file diff --git a/app/static/js/table_recap.js b/app/static/js/table_recap.js index 803daee218967a4f94d252d2d3524053e88c9921..322b2764cfaf045991545bde791707ce7ef13a62 100644 --- a/app/static/js/table_recap.js +++ b/app/static/js/table_recap.js @@ -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',