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)
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(

View File

@ -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)

View File

@ -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

View File

@ -25,7 +25,7 @@
#
##############################################################################
"""Vérification des abasneces à une évaluation
"""Vérification des absences à une évaluation
"""
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",
"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",

View File

@ -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:

View File

@ -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"<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(
formsemestre: FormSemestre,
res: NotesTableCompat,
@ -448,20 +423,20 @@ def _gen_formsemestre_recapcomplet_html(
H.append(
f"""
<thead>
{_gen_row(column_ids, titles, "th")}
{scu.gen_row(column_ids, titles, "th")}
</thead>
"""
)
# body
H.append("<tbody>")
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")
# footer
H.append("<tfoot>")
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(
"""
</tfoot>

View File

@ -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"<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
def is_entreprises_enabled():
from app.models import ScoDocSiteConfig

View File

@ -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;
}
@ -3757,3 +3768,77 @@ 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;
}

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_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,