Page état de évaluations (closes #142). Améliore tableau recap. Cosmétique.

This commit is contained in:
Emmanuel Viennet 2022-04-10 17:38:59 +02:00
parent 705aa54d77
commit 570e2dc308
12 changed files with 359 additions and 52 deletions

View File

@ -92,6 +92,8 @@ class ModuleImplResults:
ou NaN si les évaluations (dans lesquelles l'étudiant a des notes) ou NaN si les évaluations (dans lesquelles l'étudiant a des notes)
ne donnent pas de coef vers cette UE. 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.load_notes()
self.etuds_use_session2 = pd.Series(False, index=self.evals_notes.index) 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""" """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" # ou évaluation déclarée "à prise en compte immédiate"
# Les évaluations de rattrapage et 2eme session sont toujours incomplètes # Les évaluations de rattrapage et 2eme session sont toujours incomplètes
# car on calcule leur moyenne à part. # 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 ( is_complete = (evaluation.evaluation_type == scu.EVALUATION_NORMALE) and (
evaluation.publish_incomplete evaluation.publish_incomplete or (not etudids_sans_note)
or (not (inscrits_module - set(eval_df.index)))
) )
self.evaluations_completes.append(is_complete) self.evaluations_completes.append(is_complete)
self.evaluations_completes_dict[evaluation.id] = 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) # NULL en base => ABS (= -999)
eval_df.fillna(scu.NOTES_ABSENCE, inplace=True) eval_df.fillna(scu.NOTES_ABSENCE, inplace=True)
@ -193,7 +196,9 @@ class ModuleImplResults:
return eval_df return eval_df
def _etudids(self): 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 [ return [
inscr.etudid inscr.etudid
for inscr in ModuleImpl.query.get( for inscr in ModuleImpl.query.get(

View File

@ -862,20 +862,27 @@ class ResultatsSemestre(ResultatsCache):
"_tr_class": "bottom_info", "_tr_class": "bottom_info",
"_title": "Description évaluation", "_title": "Description évaluation",
} }
first = True first_eval = True
index_col = 9000 # à droite
for modimpl in self.formsemestre.modimpls_sorted: for modimpl in self.formsemestre.modimpls_sorted:
evals = self.modimpls_results[modimpl.id].get_evaluations_completes(modimpl) evals = self.modimpls_results[modimpl.id].get_evaluations_completes(modimpl)
eval_index = len(evals) - 1 eval_index = len(evals) - 1
inscrits = {i.etudid for i in modimpl.inscriptions} inscrits = {i.etudid for i in modimpl.inscriptions}
klass = "evaluation first" if first else "evaluation" first_eval_of_mod = True
first = False for e in evals:
for i, e in enumerate(evals):
cid = f"eval_{e.id}" cid = f"eval_{e.id}"
titles[ titles[
cid cid
] = f'{modimpl.module.code} {eval_index} {e.jour.isoformat() if e.jour else ""}' ] = 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}_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 eval_index -= 1
notes_db = sco_evaluation_db.do_evaluation_get_all_notes( notes_db = sco_evaluation_db.do_evaluation_get_all_notes(
e.evaluation_id e.evaluation_id
@ -889,7 +896,15 @@ class ResultatsSemestre(ResultatsCache):
# Note manquante mais prise en compte immédiate: affiche ATT # Note manquante mais prise en compte immédiate: affiche ATT
val = scu.NOTES_ATTENTE val = scu.NOTES_ATTENTE
row[cid] = scu.fmt_note(val) 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["coef"][cid] = e.coefficient
bottom_infos["min"][cid] = "0" bottom_infos["min"][cid] = "0"
bottom_infos["max"][cid] = scu.fmt_note(e.note_max) bottom_infos["max"][cid] = scu.fmt_note(e.note_max)

View File

@ -9,7 +9,6 @@ from app.comp import df_cache
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.modules import Module from app.models.modules import Module
import app.scodoc.notesdb as ndb
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu

View File

@ -25,7 +25,7 @@
# #
############################################################################## ##############################################################################
"""Vérification des abasneces à une évaluation """Vérification des absences à une évaluation
""" """
from flask import url_for, g from flask import url_for, g

View File

@ -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 '<div class="evaluations_recap"><div class="message">aucune évaluation</div></div>'
H = [
html_sco_header.sco_header(
page_title="Évaluations du semestre",
javascripts=["js/evaluations_recap.js"],
),
f"""<div class="evaluations_recap"><table class="evaluations_recap compact {
'apc' if formsemestre.formation.is_apc() else 'classic'
}"
data-filename="{filename}">""",
]
# header
H.append(
f"""
<thead>
{scu.gen_row(column_ids, titles, "th", with_col_classes=True)}
</thead>
"""
)
# body
H.append("<tbody>")
for row in rows:
H.append(f"{scu.gen_row(column_ids, row, with_col_classes=True)}\n")
H.append("""</tbody></table></div>""")
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

View File

@ -357,6 +357,11 @@ def formsemestre_status_menubar(sem):
"endpoint": "notes.formsemestre_recapcomplet", "endpoint": "notes.formsemestre_recapcomplet",
"args": {"formsemestre_id": formsemestre_id}, "args": {"formsemestre_id": formsemestre_id},
}, },
{
"title": "État des évaluations",
"endpoint": "notes.evaluations_recap",
"args": {"formsemestre_id": formsemestre_id},
},
{ {
"title": "Saisie des notes", "title": "Saisie des notes",
"endpoint": "notes.formsemestre_status", "endpoint": "notes.formsemestre_status",

View File

@ -698,7 +698,7 @@ def _add_eval_columns(
# calcul moyenne SANS LES ABSENTS ni les DEMISSIONNAIRES # calcul moyenne SANS LES ABSENTS ni les DEMISSIONNAIRES
if ( if (
(etudid in inscrits) (etudid in inscrits)
and val != None and val is not None
and val != scu.NOTES_NEUTRALISE and val != scu.NOTES_NEUTRALISE
and val != scu.NOTES_ATTENTE and val != scu.NOTES_ATTENTE
): ):
@ -721,16 +721,26 @@ def _add_eval_columns(
comment, comment,
) )
else: else:
explanation = "" if (etudid in inscrits) and e["publish_incomplete"]:
val_fmt = "" # Note manquante mais prise en compte immédiate: affiche ATT
val = None 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: 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", ""): if not row.get("_css_row_class", ""):
row["_css_row_class"] = "etudabs" row["_css_row_class"] = "etudabs"
else: else:
row[f"_{evaluation_id}_td_attrs"] = f'class="{klass}" ' row[f"_{evaluation_id}_td_attrs"] = f'class="{cell_class}" '
# regroupe les commentaires # regroupe les commentaires
if explanation: if explanation:
if explanation in K: if explanation in K:

View File

@ -357,31 +357,6 @@ def formsemestres_bulletins(annee_scolaire):
return scu.sendJSON(js_list) 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"<a {href} {target_attrs}>{content}</a>"
return f"<{elt} {attrs}>{content}</{elt}>"
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'<tr {tr_id} {tr_class}>{"".join([_gen_cell(key, row, elt) for key in keys])}</tr>'
def gen_formsemestre_recapcomplet_html( def gen_formsemestre_recapcomplet_html(
formsemestre: FormSemestre, formsemestre: FormSemestre,
res: NotesTableCompat, res: NotesTableCompat,
@ -448,20 +423,20 @@ def _gen_formsemestre_recapcomplet_html(
H.append( H.append(
f""" f"""
<thead> <thead>
{_gen_row(column_ids, titles, "th")} {scu.gen_row(column_ids, titles, "th")}
</thead> </thead>
""" """
) )
# body # body
H.append("<tbody>") H.append("<tbody>")
for row in rows: 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("</tbody>\n") H.append("</tbody>\n")
# footer # footer
H.append("<tfoot>") H.append("<tfoot>")
idx_last = len(footer_rows) - 1 idx_last = len(footer_rows) - 1
for i, row in enumerate(footer_rows): 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( H.append(
""" """
</tfoot> </tfoot>

View File

@ -1077,6 +1077,36 @@ def objects_renumber(db, obj_list) -> None:
db.session.commit() 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"<a {href} {target_attrs}>{content}</a>"
return f"<{elt} {attrs}>{content}</{elt}>"
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"""<tr {tr_id} {tr_class}>{"".join([gen_cell(key, row, elt, with_col_class=with_col_classes) for key in keys if not key.startswith('_')])}</tr>"""
# Pour accès depuis les templates jinja # Pour accès depuis les templates jinja
def is_entreprises_enabled(): def is_entreprises_enabled():
from app.models import ScoDocSiteConfig from app.models import ScoDocSiteConfig

View File

@ -1076,7 +1076,7 @@ tr.etuddem td {
td.etudabs, td.etudabs,
td.etudabs a.discretelink, td.etudabs a.discretelink,
tr.etudabs td.moyenne a.discretelink { tr.etudabs td.moyenne a.discretelink {
color: rgb(180, 0, 0); color: rgb(195, 0, 0);
} }
tr.moyenne td { tr.moyenne td {
@ -1103,6 +1103,17 @@ table.notes_evaluation th.eval_attente {
width: 80px; 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 { table.notes_evaluation tr td a.discretelink:hover {
text-decoration: none; text-decoration: none;
} }
@ -3554,7 +3565,7 @@ table.dataTable td.group {
text-align: left; text-align: left;
} }
/* Nouveau tableau recap */ /* ------------- Nouveau tableau recap ------------ */
div.table_recap { div.table_recap {
margin-top: 6px; margin-top: 6px;
} }
@ -3756,4 +3767,78 @@ table.table_recap tr.apo td {
table.table_recap td.evaluation.first, table.table_recap td.evaluation.first,
table.table_recap th.evaluation.first { table.table_recap th.evaluation.first {
border-left: 2px solid rgb(4, 16, 159); 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;
} }

View File

@ -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
},
],
})
});

View File

@ -100,6 +100,7 @@ from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_check_abs from app.scodoc import sco_evaluation_check_abs
from app.scodoc import sco_evaluation_db from app.scodoc import sco_evaluation_db
from app.scodoc import sco_evaluation_edit 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_export_results
from app.scodoc import sco_formations from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre 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_inscriptions
from app.scodoc import sco_formsemestre_status from app.scodoc import sco_formsemestre_status
from app.scodoc import sco_formsemestre_validation 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_inscr_passage
from app.scodoc import sco_liste_notes from app.scodoc import sco_liste_notes
from app.scodoc import sco_lycee from app.scodoc import sco_lycee
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
from app.scodoc import sco_moduleimpl_inscriptions from app.scodoc import sco_moduleimpl_inscriptions
from app.scodoc import sco_moduleimpl_status 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_permissions_check
from app.scodoc import sco_placement from app.scodoc import sco_placement
from app.scodoc import sco_poursuite_dut from app.scodoc import sco_poursuite_dut
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_prepajury from app.scodoc import sco_prepajury
from app.scodoc import sco_pvjury from app.scodoc import sco_pvjury
from app.scodoc import sco_pvpdf
from app.scodoc import sco_recapcomplet from app.scodoc import sco_recapcomplet
from app.scodoc import sco_report from app.scodoc import sco_report
from app.scodoc import sco_saisie_notes 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_users
from app.scodoc import sco_xml from app.scodoc import sco_xml
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_pdf import PDFLOCK
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.TrivialFormulator import TrivialFormulator
from app.views import ScoData from app.views import ScoData
@ -235,6 +231,11 @@ sco_publish(
sco_recapcomplet.formsemestre_recapcomplet, sco_recapcomplet.formsemestre_recapcomplet,
Permission.ScoView, Permission.ScoView,
) )
sco_publish(
"/evaluations_recap",
sco_evaluation_recap.evaluations_recap,
Permission.ScoView,
)
sco_publish( sco_publish(
"/formsemestres_bulletins", "/formsemestres_bulletins",
sco_recapcomplet.formsemestres_bulletins, sco_recapcomplet.formsemestres_bulletins,