diff --git a/app/but/jury_but_recap.py b/app/but/jury_but_recap.py
index 4d8e5922b5..12225fd8e3 100644
--- a/app/but/jury_but_recap.py
+++ b/app/but/jury_but_recap.py
@@ -4,7 +4,7 @@
# See LICENSE
##############################################################################
-"""Jury BUT: table recap annuelle et liens saisie
+"""Jury BUT et classiques: table recap annuelle et liens saisie
"""
import collections
@@ -19,15 +19,15 @@ from app.but.jury_but import (
DecisionsProposeesUE,
)
from app.comp.res_but import ResultatsSemestreBUT
+from app.comp.res_compat import NotesTableCompat
from app.comp import res_sem
+from app.models import UniteEns
from app.models.etudiants import Identite
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences
from app.models.formsemestre import FormSemestre
from app.scodoc import html_sco_header
from app.scodoc.sco_codes_parcours import (
BUT_BARRE_RCUE,
- BUT_BARRE_UE,
- BUT_BARRE_UE8,
BUT_RCUE_SUFFISANT,
)
from app.scodoc import sco_formsemestre_status
@@ -39,16 +39,97 @@ from app.tables.recap import RowRecap, TableRecap
class TableJury(TableRecap):
"""Cette table recap reprend les colonnes du tableau recap, sauf les évaluations,
et ajoute:
- - les RCUEs
- - le lien de saisie ou modif de la décision de jury
+ Pour le BUT:
+ - les RCUEs (moyenne et code décision)
+ Pour toutes les formations:
+ - les codes de décisions jury sur les UEs
+ - le lien de saisie ou modif de la décision de jury
"""
+ def __init__(self, *args, row_class: str = None, read_only=True, **kwargs):
+ super().__init__(
+ *args, row_class=row_class or RowJury, finalize=False, **kwargs
+ )
+ # redéclare pour VSCode
+ self.rows: list["RowJury"] = self.rows
+ self.res: NotesTableCompat = self.res
+ self.read_only = read_only
+ # Stats jury: fréquence de chaque code enregistré
+ self.freq_codes_annuels = collections.Counter()
+ # Ajout colonnes spécifiques à la table jury:
+
+ if self.res.is_apc:
+ self.add_rcues()
+ self.add_jury()
+ # Termine la table
+ self.finalize()
+ self.add_groups_header()
+
+ def add_rcues(self):
+ """Ajoute les colonnes indiquant le nb de RCUEs et chaque RCUE
+ pour tous les étudiants de la table.
+ La table contient des rows avec la clé etudid.
+ Les colonnes ont la classe css "rcue".
+ """
+ self.insert_group("rcue", before="col_ues_validables")
+ for row in self.rows:
+ etud: Identite = row.etud
+ deca = row.deca
+ if deca.code_valide:
+ self.freq_codes_annuels[deca.code_valide] += 1
+ row.add_nb_rcues_cell()
+ # --- Les RCUEs
+ for rcue in deca.rcues_annee:
+ dec_rcue = deca.dec_rcue_by_ue.get(rcue.ue_1.id)
+ if dec_rcue is not None: # None si l'UE n'est pas associée à un niveau
+ row.add_rcue_cols(dec_rcue)
+
+ def add_jury(self):
+ """Ajoute la colonne code jury et le lien.
+ Le code jury est celui du semestre: cette colonne n'est montrée
+ que pour les formations classiques, ce code n'est pas utilisé en BUT.
+ """
+ res = self.res
+ if res.validations:
+ for row in self.rows:
+ etud = row.etud
+ if not res.is_apc:
+ # formations classiques: code semestre
+ dec_sem = res.validations.decisions_jury.get(etud.id)
+ jury_code_sem = dec_sem["code"] if dec_sem else ""
+ row.add_cell(
+ "jury_code_sem", "Jury", jury_code_sem, group="jury_code_sem"
+ )
+ row.add_cell(
+ "jury_link",
+ "",
+ f"""{("modifier" if res.validations.has_decision(etud) else "saisir")
+ if res.formsemestre.etat else "voir"} décisions""",
+ group="col_jury_link",
+ target=url_for(
+ "notes.formsemestre_validation_etud_form",
+ scodoc_dept=g.scodoc_dept,
+ formsemestre_id=res.formsemestre.id,
+ etudid=etud.id,
+ ),
+ target_attrs={"class": "stdlink"},
+ )
+
class RowJury(RowRecap):
"Ligne de la table saisie jury"
- def add_nb_rcues_cell(self, deca: DecisionsProposeesAnnee):
+ def __init__(self, table: TableJury, etud: Identite, *args, **kwargs):
+ self.table: TableJury = table
+ super().__init__(table, etud, *args, **kwargs)
+ # Conserve le deca de cet étudiant:
+ self.deca = jury_but.DecisionsProposeesAnnee(
+ self.etud, self.table.res.formsemestre
+ )
+
+ def add_nb_rcues_cell(self):
"cell avec nb niveaux validables / total"
+ deca = self.deca
classes = ["col_rcue", "col_rcues_validables"]
if deca.nb_rcues_under_8 > 0:
classes.append("moy_ue_warning")
@@ -78,42 +159,71 @@ class RowJury(RowRecap):
"RCUEs",
f"""{deca.nb_validables}/{deca.nb_competences}"""
+ ((" " + scu.EMO_WARNING) if deca.nb_rcues_under_8 > 0 else ""),
- group="rcues_validables",
+ raw_content=f"""{deca.nb_validables}/{deca.nb_competences}""",
+ group="rcue",
classes=classes,
data={"order": order},
)
- def add_ue_cells(self, dec_ue: DecisionsProposeesUE):
- "cell de moyenne d'UE"
- col_id = f"moy_ue_{dec_ue.ue.id}"
+ def add_ue_cols(self, ue: UniteEns, ue_status: dict):
+ "Ajoute 2 colonnes: moyenne d'UE et code jury"
+ super().add_ue_cols(ue, ue_status) # table recap standard
+ dues = self.table.res.get_etud_decision_ues(self.etud.id)
+ if not dues:
+ return
+ due = dues.get(ue.id)
+ if not due:
+ return
+ col_id = f"moy_ue_{ue.id}_code"
+ self.add_cell(
+ col_id,
+ "", # titre vide
+ due["code"],
+ raw_content=due["code"],
+ group="col_ue",
+ classes=["recorded_code"],
+ column_classes={"col_jury", "col_ue_code"},
+ target_attrs={
+ "title": f"""enregistrée le {due['event_date']}, {
+ (due["ects"] or 0):.3g} ECTS."""
+ },
+ )
+
+ def add_rcue_cols(self, dec_rcue: DecisionsProposeesRCUE):
+ "2 cells: moyenne du RCUE, code enregistré"
+ self.table.group_titles["rcue"] = "RCUEs en cours"
+ rcue = dec_rcue.rcue
+ col_id = f"moy_rcue_{rcue.ue_1.niveau_competence_id}" # le niveau_id
note_class = ""
- val = dec_ue.moy_ue
+ val = rcue.moy_rcue
if isinstance(val, float):
- if val < BUT_BARRE_UE:
- note_class = "moy_inf"
- elif val >= BUT_BARRE_UE:
+ if val < BUT_BARRE_RCUE:
+ note_class = "moy_ue_inf"
+ elif val >= BUT_BARRE_RCUE:
note_class = "moy_ue_valid"
- if val < BUT_BARRE_UE8:
+ if val < BUT_RCUE_SUFFISANT:
note_class = "moy_ue_warning" # notes très basses
self.add_cell(
col_id,
- dec_ue.ue.acronyme,
- self.fmt_note(val),
- group="col_ue",
- classes="col_ue" + note_class,
- column_class="col_ue",
+ f"
{rcue.ue_1.acronyme}
{rcue.ue_2.acronyme}
",
+ self.table.fmt_note(val),
+ raw_content=val,
+ group="rcue",
+ classes=[note_class],
+ column_classes={"col_rcue"},
)
self.add_cell(
col_id + "_code",
- dec_ue.ue.acronyme,
- dec_ue.code_valide or "",
- classes="col_ue_code recorded_code",
- column_class="col_ue",
+ f"{rcue.ue_1.acronyme}
{rcue.ue_2.acronyme}
",
+ dec_rcue.code_valide or "",
+ group="rcue",
+ classes=["col_rcue_code", "recorded_code"],
+ column_classes={"col_rcue"},
)
def formsemestre_saisie_jury_but(
- formsemestre2: FormSemestre,
+ formsemestre: FormSemestre,
read_only: bool = False,
selected_etudid: int = None,
mode="jury",
@@ -125,7 +235,6 @@ def formsemestre_saisie_jury_but(
Si mode == "recap", table recap des codes, sans liens de saisie.
"""
- # Quick & Dirty
# pour chaque etud de res2 trié
# S1: UE1, ..., UEn
# S2: UE1, ..., UEn
@@ -137,32 +246,40 @@ def formsemestre_saisie_jury_but(
# Pour le 1er etud, faire un check_ues_ready_jury(self) -> page d'erreur
# -> rcue .ue_1, .ue_2 -> stroe moy ues, rcue.moy_rcue, etc
- if formsemestre2.formation.referentiel_competence is None:
- raise ScoNoReferentielCompetences(formation=formsemestre2.formation)
+ if formsemestre.formation.referentiel_competence is None:
+ raise ScoNoReferentielCompetences(formation=formsemestre.formation)
- rows, titles, column_ids, jury_stats = get_jury_but_table(
- formsemestre2, read_only=read_only, mode=mode
+ res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
+ table = TableJury(
+ res,
+ convert_values=True,
+ mode_jury=True,
+ read_only=read_only,
+ classes=[
+ "table_jury_but_bilan" if mode == "recap" else "",
+ "table_recap",
+ "apc",
+ "jury table_jury_but",
+ ],
+ selected_row_id=selected_etudid,
)
- if not rows:
+ if table.is_empty():
return (
''
)
- filename = scu.sanitize_filename(
- f"""jury-but-{formsemestre2.titre_num()}-{time.strftime("%Y-%m-%d")}"""
- )
- klass = "table_jury_but_bilan" if mode == "recap" else ""
- table_html = build_table_jury_but_html(
- filename, rows, titles, column_ids, selected_etudid=selected_etudid, klass=klass
+ table.data["filename"] = scu.sanitize_filename(
+ f"""jury-but-{formsemestre.titre_num()}-{time.strftime("%Y-%m-%d")}"""
)
+ table_html = table.html()
H = [
html_sco_header.sco_header(
- page_title=f"{formsemestre2.sem_modalite()}: jury BUT annuel",
+ page_title=f"{formsemestre.sem_modalite()}: jury BUT",
no_side_bar=True,
init_qtip=True,
javascripts=["js/etud_info.js", "js/table_recap.js"],
),
sco_formsemestre_status.formsemestre_status_head(
- formsemestre_id=formsemestre2.id
+ formsemestre_id=formsemestre.id
),
]
if mode == "recap":
@@ -173,12 +290,12 @@ def formsemestre_saisie_jury_but(
- Tableau PV de jury
- Courriers individuels (classeur pdf)
@@ -187,9 +304,10 @@ def formsemestre_saisie_jury_but(
)
H.append(
f"""
-
+
{table_html}
-
+
+
"""
)
@@ -199,7 +317,7 @@ def formsemestre_saisie_jury_but(
f"""
Saisie des décisions du jury
"""
)
@@ -208,12 +326,12 @@ def formsemestre_saisie_jury_but(
f"""
Calcul automatique des décisions du jury
Tableau récapitulatif des décisions du jury
"""
@@ -224,19 +342,19 @@ def formsemestre_saisie_jury_but(
Nb d'étudiants avec décision annuelle:
- {sum(jury_stats["codes_annuels"].values())} / {jury_stats["nb_etuds"]}
+ {sum(table.freq_codes_annuels.values())} / {len(table)}
Codes annuels octroyés:
"""
)
- for code in sorted(jury_stats["codes_annuels"].keys()):
+ for code in sorted(table.freq_codes_annuels.keys()):
H.append(
f"""
{code} |
- {jury_stats["codes_annuels"][code]} |
+ {table.freq_codes_annuels[code]} |
{
- (100*jury_stats["codes_annuels"][code] / jury_stats["nb_etuds"]):2.1f}%
+ (100*table.freq_codes_annuels[code] / len(table)):2.1f}%
|
"""
)
@@ -346,43 +464,16 @@ class RowCollector:
self.column_classes[col_id] = column_class
self.idx += 1
- def add_rcue_cells(self, dec_rcue: DecisionsProposeesRCUE):
- "2 cells: moyenne du RCUE, code enregistré"
- rcue = dec_rcue.rcue
- col_id = f"moy_rcue_{rcue.ue_1.niveau_competence_id}" # le niveau_id
- note_class = ""
- val = rcue.moy_rcue
- if isinstance(val, float):
- if val < BUT_BARRE_RCUE:
- note_class = " moy_ue_inf"
- elif val >= BUT_BARRE_RCUE:
- note_class = " moy_ue_valid"
- if val < BUT_RCUE_SUFFISANT:
- note_class = " moy_ue_warning" # notes très basses
- self.add_cell(
- col_id,
- f"{rcue.ue_1.acronyme}
{rcue.ue_2.acronyme}
",
- self.fmt_note(val),
- "col_rcue" + note_class,
- column_class="col_rcue",
- )
- self.add_cell(
- col_id + "_code",
- f"{rcue.ue_1.acronyme}
{rcue.ue_2.acronyme}
",
- dec_rcue.code_valide or "",
- "col_rcue_code recorded_code",
- column_class="col_rcue",
- )
-
-def get_jury_but_table(
+def get_jury_but_table( # XXX A SUPPRIMER apres avoir recupéré les stats
formsemestre2: FormSemestre, read_only: bool = False, mode="jury", with_links=True
) -> tuple[list[dict], list[str], list[str], dict]:
"""Construit la table des résultats annuels pour le jury BUT
=> rows_dict, titles, column_ids, jury_stats
où jury_stats est un dict donnant des comptages sur le jury.
"""
- res2: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre2)
+
+ # /////// XXX /////// XXX //////
titles = {} # column_id : title
jury_stats = {
"nb_etuds": len(formsemestre2.etuds_inscriptions),
diff --git a/app/comp/jury.py b/app/comp/jury.py
index e41af51078..226d5a4f01 100644
--- a/app/comp/jury.py
+++ b/app/comp/jury.py
@@ -9,7 +9,7 @@
import pandas as pd
from app import db
-from app.models import FormSemestre, ScolarFormSemestreValidation, UniteEns
+from app.models import FormSemestre, Identite, ScolarFormSemestreValidation, UniteEns
from app.comp.res_cache import ResultatsCache
from app.scodoc import sco_cache
from app.scodoc import sco_codes_parcours
@@ -53,7 +53,7 @@ class ValidationsSemestre(ResultatsCache):
self.comp_decisions_jury()
def comp_decisions_jury(self):
- """Cherche les decisions du jury pour le semestre (pas les UE).
+ """Cherche les decisions du jury pour le semestre (pas les RCUE).
Calcule les attributs:
decisions_jury = { etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }}
decision_jury_ues={ etudid :
@@ -102,6 +102,12 @@ class ValidationsSemestre(ResultatsCache):
self.decisions_jury_ues = decisions_jury_ues
+ def has_decision(self, etud: Identite) -> bool:
+ """Vrai si etud a au moins une décision enregistrée depuis
+ ce semestre (quelle qu'elle soit)
+ """
+ return (etud.id in self.decisions_jury_ues) or (etud.id in self.decisions_jury)
+
def formsemestre_get_ue_capitalisees(formsemestre: FormSemestre) -> pd.DataFrame:
"""Liste des UE capitalisées (ADM) utilisables dans ce formsemestre
diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css
index 58fd7307bb..c5414db8f2 100644
--- a/app/static/css/scodoc.css
+++ b/app/static/css/scodoc.css
@@ -4020,6 +4020,11 @@ table.table_recap tbody td:hover {
text-decoration: dashed underline;
}
+table.table_recap tfoot tr td {
+ padding-left: 10px;
+ padding-right: 10px;
+}
+
/* col moy gen en gras seulement pour les form. classiques */
table.table_recap.classic td.col_moy_gen {
font-weight: bold;
@@ -4039,10 +4044,10 @@ table.table_recap .cursus {
white-space: nowrap;
}
-table.table_recap .col_ue,
-table.table_recap .col_ue_code,
-table.table_recap .col_moy_gen,
-table.table_recap .group {
+table.table_recap td.col_ue,
+table.table_recap td.col_ue_code,
+table.table_recap td.col_moy_gen,
+table.table_recap td.group {
border-left: 1px solid blue;
}
@@ -4050,7 +4055,7 @@ table.table_recap .col_ue {
font-weight: bold;
}
-table.table_recap.jury .col_ue {
+table.table_recap.jury td.col_ue {
font-weight: normal;
}
@@ -4059,29 +4064,53 @@ table.table_recap.jury .col_rcue_code {
font-weight: bold;
}
-table.table_recap.jury tr.even td.col_rcue,
-table.table_recap.jury tr.even td.col_rcue_code {
- background-color: #b0d4f8;
-}
-
table.table_recap.jury tr.odd td.col_rcue,
table.table_recap.jury tr.odd td.col_rcue_code {
- background-color: #abcdef;
+ background-color: #e0eeff;
}
-table.table_recap.jury tr.odd td.col_rcues_validables {
- background-color: #e1d3c5 !important;
+/* table.table_recap.jury tr.even td.col_rcue,
+table.table_recap.jury tr.even td.col_rcue_code {
+ background-color: #e5f2ff;
+} */
+
+/* table.table_recap.jury tr.odd td.col_rcues_validables {
+ background-color: #d5eaff !important;
}
table.table_recap.jury tr.even td.col_rcues_validables {
- background-color: #fcebda !important;
-}
+ background-color: #e5f2ff !important;
+} */
+
+
table.table_recap .group {
border-left: 1px dashed rgb(160, 160, 160);
white-space: nowrap;
}
+table.table_recap thead th {
+ border-left: 1px solid rgb(200, 200, 200);
+ border-right: 1px solid rgb(200, 200, 200);
+}
+
+table.table_recap tr.groups_header th {
+ border-bottom: none;
+ font-weight: normal;
+ font-style: italic;
+ text-align: center;
+ padding-bottom: 8px;
+}
+
+table.table_recap thead tr.titles th {
+ padding-top: 0px;
+}
+
+table.table_recap tr td.col_ue_code,
+table.table_recap tr th.col_ue_code {
+ border-left: none;
+}
+
table.table_recap .admission {
white-space: nowrap;
color: rgb(6, 73, 6);
@@ -4250,7 +4279,7 @@ table.table_recap tr.apo td {
max-width: 1px;
}
-table.table_recap tr.type_col {
+table.table_recap tr.type_col td {
font-size: 40%;
font-family: monospace;
overflow-wrap: anywhere;
diff --git a/app/static/js/table_recap.js b/app/static/js/table_recap.js
index 4b45961df8..9597f9dbb4 100644
--- a/app/static/js/table_recap.js
+++ b/app/static/js/table_recap.js
@@ -10,8 +10,6 @@ $(function () {
if (mode_jury_but_bilan) {
// table bilan décisions: cache les notes
hidden_colums = hidden_colums.concat(["col_ue", "col_rcue", "col_lien_saisie_but"]);
- } else {
- hidden_colums = hidden_colums.concat(["recorded_code"]);
}
// Etat (tri des colonnes) de la table:
diff --git a/app/tables/recap.py b/app/tables/recap.py
index 800adda1bb..e405e98732 100644
--- a/app/tables/recap.py
+++ b/app/tables/recap.py
@@ -54,8 +54,12 @@ class TableRecap(tb.Table):
convert_values=False,
include_evaluations=False,
mode_jury=False,
+ row_class=None,
+ finalize=True,
+ **kwargs,
):
- super().__init__()
+ self.rows: list["RowRecap"] = [] # juste pour que VSCode nous aide sur .rows
+ super().__init__(row_class=row_class or RowRecap, **kwargs)
self.res = res
self.include_evaluations = include_evaluations
self.mode_jury = mode_jury
@@ -72,13 +76,12 @@ class TableRecap(tb.Table):
# couples (modimpl, ue) effectivement présents dans la table:
self.modimpl_ue_ids = set()
- etuds_inscriptions = res.formsemestre.etuds_inscriptions
ues = res.formsemestre.query_ues(with_sport=True) # avec bonus
ues_sans_bonus = [ue for ue in ues if ue.type != UE_SPORT]
- for etudid in etuds_inscriptions:
+ for etudid in res.formsemestre.etuds_inscriptions:
etud = Identite.query.get(etudid)
- row = RowRecap(self, etud)
+ row = self.row_class(self, etud)
self.add_row(row)
row.add_etud_cols()
row.add_moyennes_cols(ues_sans_bonus)
@@ -100,11 +103,14 @@ class TableRecap(tb.Table):
if include_evaluations:
self.add_evaluations()
+ def finalize(self):
+ """Termine la table: ajoute ligne avec les types,
+ et ajoute classe sur les colonnes vides"""
self.mark_empty_cols()
self.add_type_row()
def mark_empty_cols(self):
- """Ajoute style "col_empty" aux colonnes de modules vides"""
+ """Ajoute classe "col_empty" aux colonnes de modules vides"""
# identifie les col. vides par la classe sur leur moyenne
row_moy = self.get_row_by_id("moy")
for col_id in self.column_ids:
@@ -275,11 +281,12 @@ class TableRecap(tb.Table):
def add_partitions(self):
"""Ajoute les colonnes indiquant les groupes
+ pour tous les étudiants de la table.
La table contient des rows avec la clé etudid.
-
- Les colonnes ont la classe css "partition"
+ Les colonnes ont la classe css "partition".
"""
self.insert_group("partition", after="identite_court")
+ self.group_titles["partition"] = "Partitions"
partitions, partitions_etud_groups = sco_groups.get_formsemestre_groups(
self.res.formsemestre.id
)
@@ -404,6 +411,7 @@ class TableRecap(tb.Table):
scodoc_dept=g.scodoc_dept,
evaluation_id=e.id,
),
+ target_attrs={"class": "stdlink"},
)
def add_admissions(self):
@@ -478,6 +486,13 @@ class RowRecap(tb.Row):
"""Ajoute colonnes étudiant: codes, noms"""
res = self.table.res
etud = self.etud
+ self.table.group_titles.update(
+ {
+ "etud_codes": "Codes",
+ "identite_detail": "",
+ "identite_court": "",
+ }
+ )
# --- Codes (seront cachés, mais exportés en excel)
self.add_cell("etudid", "etudid", etud.id, "etud_codes")
self.add_cell(
@@ -614,35 +629,11 @@ class RowRecap(tb.Row):
data={"order": self.nb_ues_validables}, # tri
)
- if table.mode_jury and res.validations:
- if res.is_apc:
- # formations BUT: pas de code semestre, concatene ceux des UEs
- dec_ues = res.validations.decisions_jury_ues.get(etud.id)
- if dec_ues:
- jury_code_sem = ",".join(
- [dec_ues[ue_id].get("code", "") for ue_id in dec_ues]
- )
- else:
- jury_code_sem = ""
- else:
- # formations classiques: code semestre
- dec_sem = res.validations.decisions_jury.get(etud.id)
- jury_code_sem = dec_sem["code"] if dec_sem else ""
- self.add_cell("jury_code_sem", "Jury", jury_code_sem, "jury_code_sem")
- self.add_cell(
- "jury_link",
- "",
- f"""{("saisir" if not jury_code_sem else "modifier")
- if res.formsemestre.etat else "voir"} décisions""",
- "col_jury_link",
- )
-
def add_ue_cols(self, ue: UniteEns, ue_status: dict):
"Ajoute résultat UE au row (colonne col_ue)"
+ # sous-classé par JuryRow pour ajouter les codes
table = self.table
+ table.group_titles["col_ue"] = "UEs du semestre"
col_id = f"moy_ue_{ue.id}"
val = ue_status["moy"]
note_class = ""
@@ -659,9 +650,9 @@ class RowRecap(tb.Row):
col_id,
ue.acronyme,
table.fmt_note(val),
- group=f"col_ue_{ue.id}",
+ group="col_ue",
classes=[note_class],
- column_classes={"col_ue", "col_moy_ue"},
+ column_classes={f"col_ue_{ue.id}", "col_moy_ue"},
)
table.foot_title_row.cells[col_id].target_attrs[
"title"
diff --git a/app/tables/table_builder.py b/app/tables/table_builder.py
index f500fb0f14..19e14ea17c 100644
--- a/app/tables/table_builder.py
+++ b/app/tables/table_builder.py
@@ -73,8 +73,10 @@ class Table(Element):
classes: list[str] = None,
attrs: dict[str, str] = None,
data: dict = None,
+ row_class=None,
):
super().__init__("table", classes=classes, attrs=attrs, data=data)
+ self.row_class = row_class or Row
self.rows: list["Row"] = []
"ordered list of Rows"
self.row_by_id: dict[str, "Row"] = {}
@@ -82,6 +84,8 @@ class Table(Element):
"ordered list of columns ids"
self.groups = []
"ordered list of column groups names"
+ self.group_titles = {}
+ "title (in header top row) for the group"
self.head = []
self.foot = []
self.column_group = {}
@@ -92,8 +96,12 @@ class Table(Element):
"l'id de la ligne sélectionnée"
self.titles = {}
"Column title: { col_id : titre }"
- self.head_title_row: "Row" = Row(self, "title_head", cell_elt="th")
- self.foot_title_row: "Row" = Row(self, "title_foot", cell_elt="th")
+ self.head_title_row: "Row" = Row(
+ self, "title_head", cell_elt="th", classes=["titles"]
+ )
+ self.foot_title_row: "Row" = Row(
+ self, "title_foot", cell_elt="th", classes=["titles"]
+ )
self.empty_cell = Cell.empty()
def _prepare(self):
@@ -109,6 +117,10 @@ class Table(Element):
"return the row, or None"
return self.row_by_id.get(row_id)
+ def __len__(self):
+ "nombre de lignes dans le corps de la table"
+ return len(self.rows)
+
def is_empty(self) -> bool:
"true if table has no rows"
return len(self.rows) == 0
@@ -166,7 +178,6 @@ class Table(Element):
def add_head_row(self, row: "Row") -> "Row":
"Add a row to table head"
- # row = Row(self, cell_elt="th", category="head")
self.head.append(row)
self.row_by_id[row.id] = row
return row
@@ -177,6 +188,16 @@ class Table(Element):
self.row_by_id[row.id] = row
return row
+ def add_groups_header(self):
+ """Insert a header line at the top of the table
+ with a multicolumn th cell per group
+ """
+ self.sort_columns()
+ groups_header = RowGroupsHeader(
+ self, "groups_header", classes=["groups_header"]
+ )
+ self.head.insert(0, groups_header)
+
def sort_rows(self, key: callable, reverse: bool = False):
"""Sort table rows"""
self.rows.sort(key=key, reverse=reverse)
@@ -418,3 +439,33 @@ class Cell(Element):
return f"{super().html_content()}"
return super().html_content()
+
+
+class RowGroupsHeader(Row):
+ """Header line at the top of the table
+ with a multicolumn th cell per group
+ """
+
+ def html_content(self):
+ """Le contenu est généré intégralement ici: un th par groupe contigu
+ Note: les colonnes doivent avoir déjà été triées par table.sort_columns()
+ """
+ column_ids = self.table.column_ids
+ nb_cols = len(self.table.column_ids)
+ elements = []
+ idx = 0
+ while idx < nb_cols:
+ group_title = self.table.group_titles.get(
+ self.table.column_group.get(column_ids[idx])
+ )
+ colspan = 1
+ idx += 1
+ # on groupe tant que les TITRES des groupes sont identiques
+ while idx < nb_cols and group_title == self.table.group_titles.get(
+ self.table.column_group.get(column_ids[idx])
+ ):
+ idx += 1
+ colspan += 1
+ elements.append(f"""{group_title or ""} | """)
+
+ return "\n".join(elements) if len(elements) > 1 else ""
diff --git a/app/views/notes.py b/app/views/notes.py
index d5b9bca9fc..4ae34dd768 100644
--- a/app/views/notes.py
+++ b/app/views/notes.py
@@ -2891,7 +2891,7 @@ def formsemestre_jury_but_erase(
return render_template(
"confirm_dialog.j2",
title=f"""Effacer les validations de jury {
- ("de" + etud.nomprenom)
+ ("de " + etud.nomprenom)
if etud
else ("des " + str(len(etuds)) + " étudiants inscrits dans ce semestre")
} ?""",