1
0
forked from ScoDoc/ScoDoc

WIP: table jury.

This commit is contained in:
Emmanuel Viennet 2023-02-05 18:38:52 +01:00
parent d4da019c2e
commit 724d01c36a
7 changed files with 305 additions and 139 deletions

View File

@ -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"<div>{rcue.ue_1.acronyme}</div><div>{rcue.ue_2.acronyme}</div>",
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"<div>{rcue.ue_1.acronyme}</div><div>{rcue.ue_2.acronyme}</div>",
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 (
'<div class="table_recap"><div class="message">aucun étudiant !</div></div>'
)
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(
<ul>
<li><a href="{url_for(
"notes.pvjury_table_but",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre2.id)
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
}" class="stdlink">Tableau PV de jury</a>
</li>
<li><a href="{url_for(
"notes.formsemestre_lettres_individuelles",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre2.id)
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
}" class="stdlink">Courriers individuels (classeur pdf)</a>
</li>
</div>
@ -187,9 +304,10 @@ def formsemestre_saisie_jury_but(
)
H.append(
f"""
<div class="table_recap">
{table_html}
</div>
<div class="table_jury_but_links">
"""
)
@ -199,7 +317,7 @@ def formsemestre_saisie_jury_but(
f"""
<p><a class="stdlink" href="{url_for(
"notes.formsemestre_saisie_jury",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre2.id)
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
}">Saisie des décisions du jury</a>
</p>"""
)
@ -208,12 +326,12 @@ def formsemestre_saisie_jury_but(
f"""
<p><a class="stdlink" href="{url_for(
"notes.formsemestre_validation_auto_but",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre2.id)
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
}">Calcul automatique des décisions du jury</a>
</p>
<p><a class="stdlink" href="{url_for(
"notes.formsemestre_jury_but_recap",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre2.id)
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
}">Tableau récapitulatif des décisions du jury</a>
</p>
"""
@ -224,19 +342,19 @@ def formsemestre_saisie_jury_but(
<div class="jury_stats">
<div>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)}
</div>
<div><b>Codes annuels octroyés:</b></div>
<table class="jury_stats_codes">
"""
)
for code in sorted(jury_stats["codes_annuels"].keys()):
for code in sorted(table.freq_codes_annuels.keys()):
H.append(
f"""<tr>
<td>{code}</td>
<td style="text-align:right">{jury_stats["codes_annuels"][code]}</td>
<td style="text-align:right">{table.freq_codes_annuels[code]}</td>
<td style="text-align:right">{
(100*jury_stats["codes_annuels"][code] / jury_stats["nb_etuds"]):2.1f}%
(100*table.freq_codes_annuels[code] / len(table)):2.1f}%
</td>
</tr>"""
)
@ -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"<div>{rcue.ue_1.acronyme}</div><div>{rcue.ue_2.acronyme}</div>",
self.fmt_note(val),
"col_rcue" + note_class,
column_class="col_rcue",
)
self.add_cell(
col_id + "_code",
f"<div>{rcue.ue_1.acronyme}</div><div>{rcue.ue_2.acronyme}</div>",
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
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),

View File

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

View File

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

View File

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

View File

@ -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"""<a href="{url_for('notes.formsemestre_validation_etud_form',
scodoc_dept=g.scodoc_dept, formsemestre_id=res.formsemestre.id, etudid=etud.id
)
}">{("saisir" if not jury_code_sem else "modifier")
if res.formsemestre.etat else "voir"} décisions</a>""",
"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"

View File

@ -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"<a {href} {target_attrs_str}>{super().html_content()}</a>"
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"""<th colspan="{colspan}">{group_title or ""}</th>""")
return "\n".join(elements) if len(elements) > 1 else ""

View File

@ -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")
} ?""",