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 '
'
+ H = [
+ html_sco_header.sco_header(
+ page_title="Évaluations du semestre",
+ javascripts=["js/evaluations_recap.js"],
+ ),
+ f"""""")
+ 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}{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'{"".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}{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"""{"".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,