From 570e2dc308533aa819dd978e34ecc99ff448e37b Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 10 Apr 2022 17:38:59 +0200 Subject: [PATCH] =?UTF-8?q?Page=20=C3=A9tat=20de=20=C3=A9valuations=20(clo?= =?UTF-8?q?ses=20#142).=20Am=C3=A9liore=20tableau=20recap.=20Cosm=C3=A9tiq?= =?UTF-8?q?ue.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/comp/moy_mod.py | 11 +- app/comp/res_common.py | 27 +++-- app/models/moduleimpls.py | 1 - app/scodoc/sco_evaluation_check_abs.py | 2 +- app/scodoc/sco_evaluation_recap.py | 144 +++++++++++++++++++++++++ app/scodoc/sco_formsemestre_status.py | 5 + app/scodoc/sco_liste_notes.py | 22 ++-- app/scodoc/sco_recapcomplet.py | 31 +----- app/scodoc/sco_utils.py | 30 ++++++ app/static/css/scodoc.css | 89 ++++++++++++++- app/static/js/evaluations_recap.js | 38 +++++++ app/views/notes.py | 11 +- 12 files changed, 359 insertions(+), 52 deletions(-) create mode 100644 app/scodoc/sco_evaluation_recap.py create mode 100644 app/static/js/evaluations_recap.js diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py index eea357e8f..13829a392 100644 --- a/app/comp/moy_mod.py +++ b/app/comp/moy_mod.py @@ -92,6 +92,8 @@ class ModuleImplResults: ou NaN si les évaluations (dans lesquelles l'étudiant a des notes) ne donnent pas de coef vers cette UE. """ + self.evals_etudids_sans_note = {} + """dict: evaluation_id : set des etudids non notés dans cette eval, sans les démissions.""" self.load_notes() self.etuds_use_session2 = pd.Series(False, index=self.evals_notes.index) """1 bool par etud, indique si sa moyenne de module vient de la session2""" @@ -142,12 +144,13 @@ class ModuleImplResults: # ou évaluation déclarée "à prise en compte immédiate" # Les évaluations de rattrapage et 2eme session sont toujours incomplètes # car on calcule leur moyenne à part. + etudids_sans_note = inscrits_module - set(eval_df.index) # sans les dem. is_complete = (evaluation.evaluation_type == scu.EVALUATION_NORMALE) and ( - evaluation.publish_incomplete - or (not (inscrits_module - set(eval_df.index))) + evaluation.publish_incomplete or (not etudids_sans_note) ) self.evaluations_completes.append(is_complete) self.evaluations_completes_dict[evaluation.id] = is_complete + self.evals_etudids_sans_note[evaluation.id] = etudids_sans_note # NULL en base => ABS (= -999) eval_df.fillna(scu.NOTES_ABSENCE, inplace=True) @@ -193,7 +196,9 @@ class ModuleImplResults: return eval_df def _etudids(self): - """L'index du dataframe est la liste de tous les étudiants inscrits au semestre""" + """L'index du dataframe est la liste de tous les étudiants inscrits au semestre + (incluant les DEM et DEF) + """ return [ inscr.etudid for inscr in ModuleImpl.query.get( diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 60c7547db..7bc199ead 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -862,20 +862,27 @@ class ResultatsSemestre(ResultatsCache): "_tr_class": "bottom_info", "_title": "Description évaluation", } - first = True + first_eval = True + index_col = 9000 # à droite 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): + first_eval_of_mod = True + for e in evals: cid = f"eval_{e.id}" titles[ cid ] = f'{modimpl.module.code} {eval_index} {e.jour.isoformat() if e.jour else ""}' + klass = "evaluation" + if first_eval: + klass += " first" + elif first_eval_of_mod: + klass += " first_of_mod" titles[f"_{cid}_class"] = klass - titles[f"_{cid}_col_order"] = 9000 + i # à droite + first_eval_of_mod = first_eval = False + titles[f"_{cid}_col_order"] = index_col + index_col += 1 eval_index -= 1 notes_db = sco_evaluation_db.do_evaluation_get_all_notes( e.evaluation_id @@ -889,7 +896,15 @@ class ResultatsSemestre(ResultatsCache): # 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 + row[f"_{cid}_class"] = klass + { + "ABS": " abs", + "ATT": " att", + "EXC": " exc", + }.get(row[cid], "") + else: + row[cid] = "ni" + row[f"_{cid}_class"] = klass + " non_inscrit" + bottom_infos["coef"][cid] = e.coefficient bottom_infos["min"][cid] = "0" bottom_infos["max"][cid] = scu.fmt_note(e.note_max) diff --git a/app/models/moduleimpls.py b/app/models/moduleimpls.py index 292ec8ffd..7574ed7e8 100644 --- a/app/models/moduleimpls.py +++ b/app/models/moduleimpls.py @@ -9,7 +9,6 @@ from app.comp import df_cache from app.models.etudiants import Identite from app.models.modules import Module -import app.scodoc.notesdb as ndb from app.scodoc import sco_utils as scu diff --git a/app/scodoc/sco_evaluation_check_abs.py b/app/scodoc/sco_evaluation_check_abs.py index b8287c25d..b464cd673 100644 --- a/app/scodoc/sco_evaluation_check_abs.py +++ b/app/scodoc/sco_evaluation_check_abs.py @@ -25,7 +25,7 @@ # ############################################################################## -"""Vérification des abasneces à une évaluation +"""Vérification des absences à une évaluation """ from flask import url_for, g diff --git a/app/scodoc/sco_evaluation_recap.py b/app/scodoc/sco_evaluation_recap.py new file mode 100644 index 000000000..168b463b8 --- /dev/null +++ b/app/scodoc/sco_evaluation_recap.py @@ -0,0 +1,144 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +"""Tableau recap. de toutes les évaluations d'un semestre +avec leur état. + +Sur une idée de Pascal Bouron, de Lyon. +""" +import time +from flask import g, url_for + +from app.models import Evaluation, FormSemestre +from app.comp import res_sem +from app.comp.res_compat import NotesTableCompat +from app.comp.moy_mod import ModuleImplResults +from app.scodoc import html_sco_header +import app.scodoc.sco_utils as scu + + +def evaluations_recap(formsemestre_id: int) -> str: + """Page récap. de toutes les évaluations d'un semestre""" + formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) + rows, titles = evaluations_recap_table(formsemestre) + column_ids = titles.keys() + filename = scu.sanitize_filename( + f"""evaluations-{formsemestre.titre_num()}-{time.strftime("%Y-%m-%d")}""" + ) + if not rows: + return '
aucune évaluation
' + H = [ + html_sco_header.sco_header( + page_title="Évaluations du semestre", + javascripts=["js/evaluations_recap.js"], + ), + f"""
""", + ] + # header + H.append( + f""" + + {scu.gen_row(column_ids, titles, "th", with_col_classes=True)} + + """ + ) + # body + H.append("") + for row in rows: + H.append(f"{scu.gen_row(column_ids, row, with_col_classes=True)}\n") + + H.append("""
""") + H.append( + html_sco_header.sco_footer(), + ) + return "".join(H) + + +def evaluations_recap_table(formsemestre: FormSemestre) -> list[dict]: + """Tableau recap. de toutes les évaluations d'un semestre + Colonnes: + - code (UE ou module), + - titre + - complete + - publiée + - inscrits (non dem. ni def.) + - nb notes manquantes + - nb ATT + - nb ABS + - nb EXC + """ + rows = [] + titles = { + "type": "", + "code": "Code", + "titre": "", + "date": "Date", + "complete": "Comptée", + "inscrits": "Inscrits", + "manquantes": "Manquantes", # notes eval non entrées + "nb_abs": "ABS", + "nb_att": "ATT", + "nb_exc": "EXC", + } + nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) + line_idx = 0 + for modimpl in nt.formsemestre.modimpls_sorted: + modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl.id] + row = { + "type": modimpl.module.type_abbrv().upper(), + "_type_order": f"{line_idx:04d}", + "code": modimpl.module.code, + "_code_target": url_for( + "notes.moduleimpl_status", + scodoc_dept=g.scodoc_dept, + moduleimpl_id=modimpl.id, + ), + "titre": modimpl.module.titre, + "_titre_class": "titre", + "inscrits": modimpl_results.nb_inscrits_module, + "date": "-", + "_date_order": "", + "_tr_class": f"module {modimpl.module.type_abbrv()}", + } + rows.append(row) + line_idx += 1 + for evaluation_id in modimpl_results.evals_notes: + e = Evaluation.query.get(evaluation_id) + eval_etat = modimpl_results.evaluations_etat[evaluation_id] + row = { + "type": "", + "_type_order": f"{line_idx:04d}", + "titre": e.description or "sans titre", + "_titre_target": url_for( + "notes.evaluation_listenotes", + scodoc_dept=g.scodoc_dept, + evaluation_id=evaluation_id, + ), + "_titre_target_attrs": 'class="discretelink"', + "date": e.jour.strftime("%d/%m/%Y") if e.jour else "", + "_date_order": e.jour.isoformat() if e.jour else "", + "complete": "oui" if eval_etat.is_complete else "non", + "_complete_target": "#", + "_complete_target_attrs": 'class="bull_link" title="prise en compte dans les moyennes"' + if eval_etat.is_complete + else 'class="bull_link incomplete" title="il manque des notes"', + "manquantes": len(modimpl_results.evals_etudids_sans_note[e.id]), + "inscrits": modimpl_results.nb_inscrits_module, + "nb_abs": sum(modimpl_results.evals_notes[e.id] == scu.NOTES_ABSENCE), + "nb_att": eval_etat.nb_attente, + "nb_exc": sum( + modimpl_results.evals_notes[e.id] == scu.NOTES_NEUTRALISE + ), + "_tr_class": "evaluation" + + (" incomplete" if not eval_etat.is_complete else ""), + } + rows.append(row) + line_idx += 1 + + return rows, titles diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 0a36697b7..802399f2c 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -357,6 +357,11 @@ def formsemestre_status_menubar(sem): "endpoint": "notes.formsemestre_recapcomplet", "args": {"formsemestre_id": formsemestre_id}, }, + { + "title": "État des évaluations", + "endpoint": "notes.evaluations_recap", + "args": {"formsemestre_id": formsemestre_id}, + }, { "title": "Saisie des notes", "endpoint": "notes.formsemestre_status", diff --git a/app/scodoc/sco_liste_notes.py b/app/scodoc/sco_liste_notes.py index fd9b7e592..cee1e8493 100644 --- a/app/scodoc/sco_liste_notes.py +++ b/app/scodoc/sco_liste_notes.py @@ -698,7 +698,7 @@ def _add_eval_columns( # calcul moyenne SANS LES ABSENTS ni les DEMISSIONNAIRES if ( (etudid in inscrits) - and val != None + and val is not None and val != scu.NOTES_NEUTRALISE and val != scu.NOTES_ATTENTE ): @@ -721,16 +721,26 @@ def _add_eval_columns( comment, ) else: - explanation = "" - val_fmt = "" - val = None + if (etudid in inscrits) and e["publish_incomplete"]: + # Note manquante mais prise en compte immédiate: affiche ATT + val = scu.NOTES_ATTENTE + val_fmt = "ATT" + explanation = "non saisie mais prise en compte immédiate" + else: + explanation = "" + val_fmt = "" + val = None + + cell_class = klass + {"ATT": " att", "ABS": " abs", "EXC": " exc"}.get( + val_fmt, "" + ) if val is None: - row[f"_{evaluation_id}_td_attrs"] = f'class="etudabs {klass}" ' + row[f"_{evaluation_id}_td_attrs"] = f'class="etudabs {cell_class}" ' if not row.get("_css_row_class", ""): row["_css_row_class"] = "etudabs" else: - row[f"_{evaluation_id}_td_attrs"] = f'class="{klass}" ' + row[f"_{evaluation_id}_td_attrs"] = f'class="{cell_class}" ' # regroupe les commentaires if explanation: if explanation in K: diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py index 0dc6b19c4..83dd79d66 100644 --- a/app/scodoc/sco_recapcomplet.py +++ b/app/scodoc/sco_recapcomplet.py @@ -357,31 +357,6 @@ def formsemestres_bulletins(annee_scolaire): return scu.sendJSON(js_list) -def _gen_cell(key: str, row: dict, elt="td"): - "html table cell" - klass = row.get(f"_{key}_class") - attrs = f'class="{klass}"' if klass else "" - order = row.get(f"_{key}_order") - if order: - attrs += f' data-order="{order}"' - content = row.get(key, "") - target = row.get(f"_{key}_target") - target_attrs = row.get(f"_{key}_target_attrs", "") - if target or target_attrs: # avec lien - href = f'href="{target}"' if target else "" - content = f"{content}" - return f"<{elt} {attrs}>{content}" - - -def _gen_row(keys: list[str], row, elt="td", selected_etudid=None): - klass = row.get("_tr_class") - tr_class = f'class="{klass}"' if klass else "" - tr_id = ( - f"""id="row_selected" """ if (row.get("etudid", "") == selected_etudid) else "" - ) - return f'{"".join([_gen_cell(key, row, elt) for key in keys])}' - - def gen_formsemestre_recapcomplet_html( formsemestre: FormSemestre, res: NotesTableCompat, @@ -448,20 +423,20 @@ def _gen_formsemestre_recapcomplet_html( H.append( f""" - {_gen_row(column_ids, titles, "th")} + {scu.gen_row(column_ids, titles, "th")} """ ) # body H.append("") for row in rows: - H.append(f"{_gen_row(column_ids, row, selected_etudid=selected_etudid)}\n") + H.append(f"{scu.gen_row(column_ids, row, selected_etudid=selected_etudid)}\n") H.append("\n") # footer H.append("") idx_last = len(footer_rows) - 1 for i, row in enumerate(footer_rows): - H.append(f'{_gen_row(column_ids, row, "th" if i == idx_last else "td")}\n') + H.append(f'{scu.gen_row(column_ids, row, "th" if i == idx_last else "td")}\n') H.append( """ diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 543d97074..c7422ecbc 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -1077,6 +1077,36 @@ def objects_renumber(db, obj_list) -> None: db.session.commit() +def gen_cell(key: str, row: dict, elt="td", with_col_class=False): + "html table cell" + klass = row.get(f"_{key}_class", "") + if with_col_class: + klass = key + " " + klass + attrs = f'class="{klass}"' if klass else "" + order = row.get(f"_{key}_order") + if order: + attrs += f' data-order="{order}"' + content = row.get(key, "") + target = row.get(f"_{key}_target") + target_attrs = row.get(f"_{key}_target_attrs", "") + if target or target_attrs: # avec lien + href = f'href="{target}"' if target else "" + content = f"{content}" + return f"<{elt} {attrs}>{content}" + + +def gen_row( + keys: list[str], row, elt="td", selected_etudid=None, with_col_classes=False +): + "html table row" + klass = row.get("_tr_class") + tr_class = f'class="{klass}"' if klass else "" + tr_id = ( + f"""id="row_selected" """ if (row.get("etudid", "") == selected_etudid) else "" + ) + return f"""{"".join([gen_cell(key, row, elt, with_col_class=with_col_classes) for key in keys if not key.startswith('_')])}""" + + # Pour accès depuis les templates jinja def is_entreprises_enabled(): from app.models import ScoDocSiteConfig diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 470b929a8..57a951b3a 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -1076,7 +1076,7 @@ tr.etuddem td { td.etudabs, td.etudabs a.discretelink, tr.etudabs td.moyenne a.discretelink { - color: rgb(180, 0, 0); + color: rgb(195, 0, 0); } tr.moyenne td { @@ -1103,6 +1103,17 @@ table.notes_evaluation th.eval_attente { width: 80px; } +table.notes_evaluation td.att a { + color: rgb(255, 0, 217); + font-weight: bold; +} + +table.notes_evaluation td.exc a { + font-style: italic; + color: rgb(0, 131, 0); +} + + table.notes_evaluation tr td a.discretelink:hover { text-decoration: none; } @@ -3554,7 +3565,7 @@ table.dataTable td.group { text-align: left; } -/* Nouveau tableau recap */ +/* ------------- Nouveau tableau recap ------------ */ div.table_recap { margin-top: 6px; } @@ -3756,4 +3767,78 @@ table.table_recap tr.apo td { table.table_recap td.evaluation.first, table.table_recap th.evaluation.first { border-left: 2px solid rgb(4, 16, 159); +} + +table.table_recap td.evaluation.first_of_mod, +table.table_recap th.evaluation.first_of_mod { + border-left: 1px dashed rgb(4, 16, 159); +} + + +table.table_recap td.evaluation.att { + color: rgb(255, 0, 217); + font-weight: bold; +} + +table.table_recap td.evaluation.abs { + color: rgb(231, 0, 0); + font-weight: bold; +} + +table.table_recap td.evaluation.exc { + font-style: italic; + color: rgb(0, 131, 0); +} + +table.table_recap td.evaluation.non_inscrit { + font-style: italic; + color: rgb(101, 101, 101); +} + +/* ------------- Tableau etat evals ------------ */ + +div.evaluations_recap table.evaluations_recap { + width: auto !important; + border: 1px solid black; +} + +table.evaluations_recap tr.odd td { + background-color: #fff4e4; +} + +table.evaluations_recap tr.res td { + background-color: #f7d372; +} + +table.evaluations_recap tr.sae td { + background-color: #d8fcc8; +} + + +table.evaluations_recap tr.module td { + font-weight: bold; +} + +table.evaluations_recap tr.evaluation td.titre { + font-style: italic; + padding-left: 2em; +} + +table.evaluations_recap td.titre, +table.evaluations_recap th.titre { + max-width: 350px; +} + +table.evaluations_recap td.complete, +table.evaluations_recap th.complete { + text-align: center; +} + +table.evaluations_recap tr.evaluation.incomplete td, +table.evaluations_recap tr.evaluation.incomplete td a { + color: red; +} + +table.evaluations_recap tr.evaluation.incomplete td a.incomplete { + font-weight: bold; } \ No newline at end of file diff --git a/app/static/js/evaluations_recap.js b/app/static/js/evaluations_recap.js new file mode 100644 index 000000000..b3b60c3fb --- /dev/null +++ b/app/static/js/evaluations_recap.js @@ -0,0 +1,38 @@ +// Tableau recap evaluations du semestre +$(function () { + $('table.evaluations_recap').DataTable( + { + paging: false, + searching: true, + info: false, + autoWidth: false, + fixedHeader: { + header: true, + footer: false + }, + orderCellsTop: true, // cellules ligne 1 pour tri + aaSorting: [], // Prevent initial sorting + colReorder: true, + "columnDefs": [ + { + // colonne date, triable (XXX ne fonctionne pas) + targets: ["date"], + "type": "string", + }, + ], + dom: 'Bfrtip', + buttons: [ + { + extend: 'copyHtml5', + text: 'Copier', + exportOptions: { orthogonal: 'export' } + }, + { + extend: 'excelHtml5', + exportOptions: { orthogonal: 'export' }, + title: document.querySelector('table.evaluations_recap').dataset.filename + }, + ], + + }) +}); diff --git a/app/views/notes.py b/app/views/notes.py index 35f92e0b6..82c455393 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -100,6 +100,7 @@ from app.scodoc import sco_evaluations from app.scodoc import sco_evaluation_check_abs from app.scodoc import sco_evaluation_db from app.scodoc import sco_evaluation_edit +from app.scodoc import sco_evaluation_recap from app.scodoc import sco_export_results from app.scodoc import sco_formations from app.scodoc import sco_formsemestre @@ -109,22 +110,18 @@ from app.scodoc import sco_formsemestre_exterieurs from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_status from app.scodoc import sco_formsemestre_validation -from app.scodoc import sco_groups from app.scodoc import sco_inscr_passage from app.scodoc import sco_liste_notes from app.scodoc import sco_lycee from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl_inscriptions from app.scodoc import sco_moduleimpl_status -from app.scodoc import sco_news -from app.scodoc import sco_parcours_dut from app.scodoc import sco_permissions_check from app.scodoc import sco_placement from app.scodoc import sco_poursuite_dut from app.scodoc import sco_preferences from app.scodoc import sco_prepajury from app.scodoc import sco_pvjury -from app.scodoc import sco_pvpdf from app.scodoc import sco_recapcomplet from app.scodoc import sco_report from app.scodoc import sco_saisie_notes @@ -136,7 +133,6 @@ from app.scodoc import sco_undo_notes from app.scodoc import sco_users from app.scodoc import sco_xml from app.scodoc.gen_tables import GenTable -from app.scodoc.sco_pdf import PDFLOCK from app.scodoc.sco_permissions import Permission from app.scodoc.TrivialFormulator import TrivialFormulator from app.views import ScoData @@ -235,6 +231,11 @@ sco_publish( sco_recapcomplet.formsemestre_recapcomplet, Permission.ScoView, ) +sco_publish( + "/evaluations_recap", + sco_evaluation_recap.evaluations_recap, + Permission.ScoView, +) sco_publish( "/formsemestres_bulletins", sco_recapcomplet.formsemestres_bulletins,