WIP: new code table recap.

This commit is contained in:
Emmanuel Viennet 2023-01-22 16:39:46 -03:00 committed by iziram
parent bbcd6d7b33
commit f0c0490816
3 changed files with 487 additions and 498 deletions

View File

@ -492,296 +492,179 @@ class TableRecap(tb.Table): # was get_table_recap
convert_values=False, convert_values=False,
include_evaluations=False, include_evaluations=False,
mode_jury=False, mode_jury=False,
): allow_html=True,
self.res = res ) -> tb.Table:
self.include_evaluations = include_evaluations """Table récap. des résultats.
self.mode_jury = mode_jury allow_html: si vrai, peut mettre du HTML dans les valeurs
Result: XXX tuple avec
- rows: liste de dicts { column_id : value }
- titles: { column_id : title }
- columns_ids: (liste des id de colonnes)
Si convert_values, transforme les notes en chaines ("12.34").
Les colonnes générées sont:
etudid
rang : rang indicatif (basé sur moy gen)
moy_gen : moy gen indicative
moy_ue_<ue_id>, ..., les moyennes d'UE
moy_res_<modimpl_id>_<ue_id>, ... les moyennes de ressources dans l'UE
moy_sae_<modimpl_id>_<ue_id>, ... les moyennes de SAE dans l'UE
On ajoute aussi des attributs: XXX
- pour les lignes:
_css_row_class (inutilisé pour le monent)
_<column_id>_class classe css:
- la moyenne générale a la classe col_moy_gen
- les colonnes SAE ont la classe col_sae
- les colonnes Resources ont la classe col_res
- les colonnes d'UE ont la classe col_ue
- les colonnes de modules (SAE ou res.) d'une UE ont la classe mod_ue_<ue_id>
_<column_id>_order : clé de tri
"""
if convert_values:
fmt_note = scu.fmt_note
else:
fmt_note = lambda x: x
parcours = self.formsemestre.formation.get_parcours() parcours = self.formsemestre.formation.get_parcours()
self.barre_moy = parcours.BARRE_MOY - scu.NOTES_TOLERANCE barre_moy = parcours.BARRE_MOY - scu.NOTES_TOLERANCE
self.barre_valid_ue = parcours.NOTES_BARRE_VALID_UE barre_valid_ue = parcours.NOTES_BARRE_VALID_UE
self.barre_warning_ue = parcours.BARRE_UE_DISPLAY_WARNING barre_warning_ue = parcours.BARRE_UE_DISPLAY_WARNING
self.cache_nomcomplet = {} # cache uid : nomcomplet NO_NOTE = "-" # contenu des cellules sans notes
if convert_values: rows = []
self.fmt_note = scu.fmt_note dict_nom_res = {} # cache uid : nomcomplet
else:
self.fmt_note = lambda x: x # def add_cell(
# couples (modimpl, ue) effectivement présents dans la table: # row: dict,
self.modimpl_ue_ids = set() # col_id: str,
# title: str,
# content: str,
# classes: str = "",
# idx: int = 100,
# ):
# "Add a cell to our table. classes is a list of css class names"
# row[col_id] = content
# if classes:
# row[f"_{col_id}_class"] = classes + f" c{idx}"
# if not col_id in titles:
# titles[col_id] = title
# titles[f"_{col_id}_col_order"] = idx
# if classes:
# titles[f"_{col_id}_class"] = classes
# return idx + 1
etuds_inscriptions = self.formsemestre.etuds_inscriptions etuds_inscriptions = self.formsemestre.etuds_inscriptions
ues = self.formsemestre.query_ues(with_sport=True) # avec bonus ues = self.formsemestre.query_ues(with_sport=True) # avec bonus
ues_sans_bonus = [ue for ue in ues if ue.type != UE_SPORT] ues_sans_bonus = [ue for ue in ues if ue.type != UE_SPORT]
modimpl_ids = set() # modimpl effectivement présents dans la table
table = tb.Table()
for etudid in etuds_inscriptions: for etudid in etuds_inscriptions:
etud = Identite.query.get(etudid) etud = Identite.query.get(etudid)
row = RowRecap(self, etudid) row = tb.Row(table, etudid)
self.add_row(row) table.add_row(row)
self.recap_add_etud(row, etud)
self._recap_add_moyennes(row, etud, ues_sans_bonus)
self.recap_add_partitions()
self.recap_add_cursus()
self._recap_add_admissions()
# tri par rang croissant
if not self.formsemestre.block_moyenne_generale:
self.sort_rows(key=lambda row: row.rang_order)
else:
self.sort_rows(key=lambda row: row.nb_ues_validables, reverse=True)
# Lignes footer (min, max, ects, apo, ...)
self.add_bottom_rows(ues_sans_bonus)
# Evaluations:
if include_evaluations:
self.add_evaluations()
self.mark_empty_cols()
self.add_type_row()
def mark_empty_cols(self):
"""Ajoute style "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:
cell: tb.Cell = row_moy.cells.get(col_id)
if cell and "col_empty" in cell.classes:
self.column_classes[col_id].append("col_empty")
def add_type_row(self):
"""Ligne avec la classe de chaque colonne recap."""
# récupère le type à partir des classes css (hack...)
row_type = tb.BottomRow(
self,
"type_col",
left_title="Type col.",
left_title_col_ids=["prenom", "nom_short"],
category="bottom_infos",
classes=["bottom_info"],
)
for col_id in self.column_ids:
group_name = self.column_group.get(col_id, "")
if group_name.startswith("col_"):
group_name = group_name[4:]
row_type.add_cell(col_id, None, group_name)
def add_bottom_rows(self, ues):
"""Les informations à mettre en bas de la table recap:
min, max, moy, ECTS, Apo."""
res = self.res
# Ordre des lignes: Min, Max, Moy, Coef, ECTS, Apo
row_min = tb.BottomRow(
self,
"min",
left_title="Min.",
left_title_col_ids=["prenom", "nom_short"],
category="bottom_infos",
classes=["bottom_info"],
)
row_max = tb.BottomRow(
self,
"max",
left_title="Max.",
left_title_col_ids=["prenom", "nom_short"],
category="bottom_infos",
classes=["bottom_info"],
)
row_moy = tb.BottomRow(
self,
"moy",
left_title="Moy.",
left_title_col_ids=["prenom", "nom_short"],
category="bottom_infos",
classes=["bottom_info"],
)
row_coef = tb.BottomRow(
self,
"coef",
left_title="Coef.",
left_title_col_ids=["prenom", "nom_short"],
category="bottom_infos",
classes=["bottom_info"],
)
row_ects = tb.BottomRow(
self,
"ects",
left_title="ECTS",
left_title_col_ids=["prenom", "nom_short"],
category="bottom_infos",
classes=["bottom_info"],
)
row_apo = tb.BottomRow(
self,
"apo",
left_title="Code Apogée",
left_title_col_ids=["prenom", "nom_short"],
category="bottom_infos",
classes=["bottom_info"],
)
# --- ECTS
# titre (à gauche) sur 2 colonnes pour s'adapter à l'affichage des noms/prenoms
for ue in ues:
col_id = f"moy_ue_{ue.id}"
row_ects.add_cell(col_id, None, ue.ects)
# ajoute cell UE vides sur ligne coef pour borders verticales
# XXX TODO classes dans table sur colonne ajoutées à tous les TD
row_coef.add_cell(col_id, None, "")
row_ects.add_cell(
"moy_gen",
None,
sum([ue.ects or 0 for ue in ues if ue.type != UE_SPORT]),
)
# --- MIN, MAX, MOY, APO
row_min.add_cell("moy_gen", None, self.fmt_note(res.etud_moy_gen.min()))
row_max.add_cell("moy_gen", None, self.fmt_note(res.etud_moy_gen.max()))
row_moy.add_cell("moy_gen", None, self.fmt_note(res.etud_moy_gen.mean()))
for ue in ues:
col_id = f"moy_ue_{ue.id}"
row_min.add_cell(col_id, None, self.fmt_note(res.etud_moy_ue[ue.id].min()))
row_max.add_cell(col_id, None, self.fmt_note(res.etud_moy_ue[ue.id].max()))
row_moy.add_cell(col_id, None, self.fmt_note(res.etud_moy_ue[ue.id].mean()))
row_apo.add_cell(col_id, None, ue.code_apogee or "")
for modimpl in res.formsemestre.modimpls_sorted:
if (modimpl.id, ue.id) in self.modimpl_ue_ids:
col_id = f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}"
if res.is_apc:
coef = res.modimpl_coefs_df[modimpl.id][ue.id]
else:
coef = modimpl.module.coefficient or 0
row_coef.add_cell(
col_id,
None,
self.fmt_note(coef),
group=f"col_ue_{ue.id}_modules",
)
notes = res.modimpl_notes(modimpl.id, ue.id)
if np.isnan(notes).all():
# aucune note valide
row_min.add_cell(col_id, None, np.nan)
row_max.add_cell(col_id, None, np.nan)
moy = np.nan
else:
row_min.add_cell(col_id, None, self.fmt_note(np.nanmin(notes)))
row_max.add_cell(col_id, None, self.fmt_note(np.nanmax(notes)))
moy = np.nanmean(notes)
row_moy.add_cell(
col_id,
None,
self.fmt_note(moy),
# aucune note dans ce module ?
classes=["col_empty" if np.isnan(moy) else ""],
)
row_apo.add_cell(col_id, None, modimpl.module.code_apogee or "")
class RowRecap(tb.Row):
"Ligne de la table recap"
def add_etud(self, etud: Identite):
"""Ajoute colonnes étudiant: codes, noms"""
res = self.table.res
# --- Codes (seront cachés, mais exportés en excel) # --- Codes (seront cachés, mais exportés en excel)
self.add_cell("etudid", "etudid", etud.id, "etud_codes") row.add_cell("etudid", "etudid", etudid, "codes")
self.add_cell( row.add_cell(
"code_nip", "code_nip",
"code_nip", "code_nip",
etud.code_nip or "", etud.code_nip or "",
"etud_codes", "codes",
) )
# --- Rang # --- Rang
if not res.formsemestre.block_moyenne_generale: if not self.formsemestre.block_moyenne_generale:
self.rang_order = res.etud_moy_gen_ranks_int[etud.id] row.add_cell(
res.add_cell(
"rang", "rang",
"Rg", "Rg",
self.etud_moy_gen_ranks[etud.id], self.etud_moy_gen_ranks[etudid],
"rang", "rang",
data={"order": f"{self.rang_order:05d}"}, data={"order": f"{self.etud_moy_gen_ranks_int[etudid]:05d}"},
) )
else:
self.rang_order = -1
# --- Identité étudiant # --- Identité étudiant
url_bulletin = url_for( url_bulletin = url_for(
"notes.formsemestre_bulletinetud", "notes.formsemestre_bulletinetud",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formsemestre_id=res.formsemestre.id, formsemestre_id=self.formsemestre.id,
etudid=etud.id, etudid=etudid,
) )
self.add_cell("civilite_str", "Civ.", etud.civilite_str, "identite_detail") row.add_cell("civilite_str", "Civ.", etud.civilite_str, "identite_detail")
self.add_cell( row.add_cell(
"nom_disp", "nom_disp",
"Nom", "Nom",
etud.nom_disp(), etud.nom_disp(),
"identite_detail", "identite_detail",
data={"order": etud.sort_key}, data={"order": etud.sort_key},
target=url_bulletin, target=url_bulletin,
target_attrs={"class": "etudinfo", "id": str(etud.id)}, target_attrs=f'class="etudinfo" id="{etudid}"',
) )
self.add_cell("prenom", "Prénom", etud.prenom, "identite_detail") row.add_cell("prenom", "Prénom", etud.prenom, "identite_detail")
self.add_cell( row.add_cell(
"nom_short", "nom_short",
"Nom", "Nom",
etud.nom_short, etud.nom_short,
"identite_court", "identite_court",
data={ data={"order": etud.sort_key},
"order": etud.sort_key,
"etudid": etud.id,
"nomprenom": etud.nomprenom,
},
target=url_bulletin, target=url_bulletin,
target_attrs={"class": "etudinfo", "id": str(etud.id)}, target_attrs=f'class="etudinfo" id="{etudid}"',
) )
def add_moyennes( # XXX was _recap_add_moyennes
self,
row: tb.Row,
etud: Identite,
ues_sans_bonus: list[UniteEns],
):
"""Ajoute cols moy_gen moy_ue et tous les modules..."""
table = self.table
res = table.res
# --- Moyenne générale # --- Moyenne générale
if not res.formsemestre.block_moyenne_generale: if not self.formsemestre.block_moyenne_generale:
moy_gen = res.etud_moy_gen.get(etud.id, False) moy_gen = self.etud_moy_gen.get(etudid, False)
note_class = "" note_class = ""
if moy_gen is False: if moy_gen is False:
moy_gen = scu.NO_NOTE_STR moy_gen = NO_NOTE
elif isinstance(moy_gen, float) and moy_gen < table.barre_moy: elif isinstance(moy_gen, float) and moy_gen < barre_moy:
note_class = "moy_ue_warning" # en rouge note_class = "moy_ue_warning" # en rouge
row.add_cell( row.add_cell(
"moy_gen", "moy_gen",
"Moy", "Moy",
table.fmt_note(moy_gen), fmt_note(moy_gen),
"col_moy_gen", "col_moy_gen",
classes=[note_class], classes=[note_class],
) )
# Ajoute bulle sur titre du pied de table: # Ajoute bulle sur titre du pied de table:
if res.is_apc: table.foot_title_row.cells["moy_gen"].target_attrs = (
table.foot_title_row.cells["moy_gen"].target_attrs[ 'title="moyenne indicative"' if self.is_apc else ""
"title" )
] = "moyenne indicative"
# --- Moyenne d'UE # --- Moyenne d'UE
self.nb_ues_validables, self.nb_ues_warning = 0, 0 nb_ues_validables, nb_ues_warning = 0, 0
for ue in ues_sans_bonus: for ue in ues_sans_bonus:
ue_status = res.get_etud_ue_status(etud.id, ue.id) ue_status = self.get_etud_ue_status(etudid, ue.id)
if ue_status is not None: if ue_status is not None:
self.add_ue(ue, ue_status) col_id = f"moy_ue_{ue.id}"
if table.mode_jury: val = ue_status["moy"]
note_class = ""
if isinstance(val, float):
if val < barre_moy:
note_class = " moy_inf"
elif val >= barre_valid_ue:
note_class = " moy_ue_valid"
nb_ues_validables += 1
if val < barre_warning_ue:
note_class = " moy_ue_warning" # notes très basses
nb_ues_warning += 1
row.add_cell(
col_id,
ue.acronyme,
fmt_note(val),
group=f"col_ue_{ue.id}",
classes=["col_ue", note_class],
)
table.foot_title_row.cells[
col_id
].target_attrs = (
f"""title="{ue.titre} S{ue.semestre_idx or '?'}" """
)
if mode_jury:
# pas d'autre colonnes de résultats # pas d'autre colonnes de résultats
continue continue
# Bonus (sport) dans cette UE ? # Bonus (sport) dans cette UE ?
# Le bonus sport appliqué sur cette UE # Le bonus sport appliqué sur cette UE
if (res.bonus_ues is not None) and (ue.id in res.bonus_ues): if (self.bonus_ues is not None) and (ue.id in self.bonus_ues):
val = res.bonus_ues[ue.id][etud.id] or "" val = self.bonus_ues[ue.id][etud.id] or ""
val_fmt = val_fmt_html = table.fmt_note(val) val_fmt = val_fmt_html = fmt_note(val)
if val: if val:
val_fmt_html = f"""<span class="green-arrow-up"></span><span class="sp2l">{ val_fmt_html = f"""<span class="green-arrow-up"></span><span class="sp2l">{
val_fmt val_fmt
@ -789,142 +672,54 @@ class RowRecap(tb.Row):
row.add_cell( row.add_cell(
f"bonus_ue_{ue.id}", f"bonus_ue_{ue.id}",
f"Bonus {ue.acronyme}", f"Bonus {ue.acronyme}",
val_fmt_html, val_fmt_html if allow_html else val_fmt,
raw_content=val_fmt,
group=f"col_ue_{ue.id}", group=f"col_ue_{ue.id}",
classes=["col_ue_bonus"], classes=["col_ue_bonus"],
raw_content=val_fmt,
) )
# Les moyennes des modules (ou ressources et SAÉs) dans cette UE # Les moyennes des modules (ou ressources et SAÉs) dans cette UE
self.add_ue_modimpls( for modimpl in self.modimpls_in_ue(ue, etudid, with_bonus=False):
ue, etud, ue_status["is_capitalized"] if ue_status["is_capitalized"]:
) # XXX _recap_add_ue_modimpls
self.nb_ues_etud_parcours = len(res.etud_ues_ids(etud.id))
ue_valid_txt = (
ue_valid_txt_html
) = f"{self.nb_ues_validables}/{self.nb_ues_etud_parcours}"
if self.nb_ues_warning:
ue_valid_txt_html += " " + scu.EMO_WARNING
# place juste avant moy. gen.
table.insert_group("col_ues_validables", before="col_moy_gen")
classes = ["col_ue"]
if self.nb_ues_warning:
classes.append("moy_ue_warning")
elif self.nb_ues_validables < len(ues_sans_bonus):
classes.append("moy_inf")
row.add_cell(
"ues_validables",
"UEs",
ue_valid_txt_html,
"col_ues_validables",
classes=classes,
raw_content=ue_valid_txt,
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(self, ue: UniteEns, ue_status: dict):
"Ajoute résultat UE au row (colonne col_ue)"
table = self.table
col_id = f"moy_ue_{ue.id}"
val = ue_status["moy"]
note_class = ""
if isinstance(val, float):
if val < table.barre_moy:
note_class = "moy_inf"
elif val >= table.barre_valid_ue:
note_class = "moy_ue_valid"
self.nb_ues_validables += 1
if val < table.barre_warning_ue:
note_class = "moy_ue_warning" # notes très basses
self.nb_ues_warning += 1
self.add_cell(
col_id,
ue.acronyme,
table.fmt_note(val),
group=f"col_ue_{ue.id}",
classes=["col_ue", note_class],
)
table.foot_title_row.cells[col_id].target_attrs[
"title"
] = f"""{ue.titre} S{ue.semestre_idx or '?'}"""
def add_ue_modimpls(
self, row: tb.Row, ue: UniteEns, etud: Identite, is_capitalized: bool
):
"""Ajoute à row les moyennes des modules (ou ressources et SAÉs) dans l'UE"""
# pylint: disable=invalid-unary-operand-type
table = row.table
res = table.res
for modimpl in res.modimpls_in_ue(ue, etud.id, with_bonus=False):
if is_capitalized:
val = "-c-" val = "-c-"
else: else:
modimpl_results = res.modimpls_results.get(modimpl.id) modimpl_results = self.modimpls_results.get(modimpl.id)
if modimpl_results: # pas bonus if modimpl_results: # pas bonus
if res.is_apc: # BUT if self.is_apc: # BUT
moys_vers_ue = modimpl_results.etuds_moy_module.get(ue.id) moys_vers_ue = modimpl_results.etuds_moy_module.get(
ue.id
)
val = ( val = (
moys_vers_ue.get(etud.id, "?") moys_vers_ue.get(etudid, "?")
if moys_vers_ue is not None if moys_vers_ue is not None
else "" else ""
) )
else: # classique: Series indépendante de l'UE else: # classique: Series indépendante de l'UE
val = modimpl_results.etuds_moy_module.get(etud.id, "?") val = modimpl_results.etuds_moy_module.get(
etudid, "?"
)
else: else:
val = "" val = ""
col_id = f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}" col_id = (
val_fmt = val_fmt_html = table.fmt_note(val) f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}"
if modimpl.module.module_type == scu.ModuleType.MALUS: )
if val and not isinstance(val, str) and not np.isnan(val): val_fmt = val_fmt_html = fmt_note(val)
if val >= 0: if convert_values and (
val_fmt_html = f"""<span class="red-arself-down"></span><span class="sp2l">+{ modimpl.module.module_type == scu.ModuleType.MALUS
val_fmt ):
}</span>""" val_fmt_html = (
else: (scu.EMO_RED_TRIANGLE_DOWN + val_fmt) if val else ""
# val_fmt_html = (scu.EMO_RED_TRIANGLE_DOWN + val_fmt) )
val_fmt_html = f"""<span class="green-arrow-up"></span><span class="sp2l malus_negatif">-{ cell = row.add_cell(
table.fmt_note(-val)}</span>"""
else:
val_fmt = val_fmt_html = "" # inscrit à ce malus, mais sans note
cell = self.add_cell(
col_id, col_id,
modimpl.module.code, modimpl.module.code,
val_fmt_html, val_fmt_html,
raw_content=val_fmt,
group=f"col_ue_{ue.id}_modules", group=f"col_ue_{ue.id}_modules",
classes=[ classes=[
f"col_{modimpl.module.type_abbrv()}", f"col_{modimpl.module.type_abbrv()}",
f"mod_ue_{ue.id}", f"mod_ue_{ue.id}",
], ],
raw_content=val_fmt,
) )
if modimpl.module.module_type == scu.ModuleType.MALUS: if modimpl.module.module_type == scu.ModuleType.MALUS:
# positionne la colonne à droite de l'UE # positionne la colonne à droite de l'UE
@ -937,15 +732,224 @@ class RowRecap(tb.Row):
moduleimpl_id=modimpl.id, moduleimpl_id=modimpl.id,
) )
nom_resp = table.cache_nomcomplet.get(modimpl.responsable_id) nom_resp = dict_nom_res.get(modimpl.responsable_id)
if nom_resp is None: if nom_resp is None:
user = User.query.get(modimpl.responsable_id) user = User.query.get(modimpl.responsable_id)
nom_resp = user.get_nomcomplet() if user else "" nom_resp = user.get_nomcomplet() if user else ""
table.cache_nomcomplet[modimpl.responsable_id] = nom_resp dict_nom_res[modimpl.responsable_id] = nom_resp
table.foot_title_row.cells[col_id].target_attrs[ table.foot_title_row.cells[
"title" col_id
] = f"{modimpl.module.titre} ({nom_resp})" ].target_attrs = (
table.modimpl_ue_ids.add((modimpl.id, ue.id)) f""" title="{modimpl.module.titre} ({nom_resp})" """
)
modimpl_ids.add(modimpl.id)
nb_ues_etud_parcours = len(self.etud_ues_ids(etudid))
ue_valid_txt = (
ue_valid_txt_html
) = f"{nb_ues_validables}/{nb_ues_etud_parcours}"
if nb_ues_warning:
ue_valid_txt_html += " " + scu.EMO_WARNING
# place juste avant moy. gen.
table.insert_group("col_ues_validables", before="moy_gen")
classes = ["col_ue"]
if nb_ues_warning:
classes.append("moy_ue_warning")
elif nb_ues_validables < len(ues_sans_bonus):
classes.append("moy_inf")
row.add_cell(
"ues_validables",
"UEs",
ue_valid_txt_html,
"col_ues_validables",
classes=classes,
raw_content=ue_valid_txt,
data={"order": nb_ues_validables}, # tri
)
if mode_jury and self.validations:
if self.is_apc:
# formations BUT: pas de code semestre, concatene ceux des UEs
dec_ues = self.validations.decisions_jury_ues.get(etudid)
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 = self.validations.decisions_jury.get(etudid)
jury_code_sem = dec_sem["code"] if dec_sem else ""
row.add_cell("jury_code_sem", "Jury", jury_code_sem, "jury_code_sem")
row.add_cell(
"jury_link",
"",
f"""<a href="{url_for('notes.formsemestre_validation_etud_form',
scodoc_dept=g.scodoc_dept, formsemestre_id=self.formsemestre.id, etudid=etudid
)
}">{"saisir" if not jury_code_sem else "modifier"} décision</a>""",
"col_jury_link",
)
self.recap_add_partitions(table)
self.recap_add_cursus(table)
self._recap_add_admissions(table)
# tri par rang croissant
if not self.formsemestre.block_moyenne_generale:
table.sort_rows(key=lambda e: e["_rang_order"])
else:
table.sort_rows(key=lambda e: e["_ues_validables_order"], reverse=True)
# INFOS POUR FOOTER
bottom_infos = self._recap_bottom_infos(
table, ues_sans_bonus, modimpl_ids, fmt_note
)
if include_evaluations:
self._recap_add_evaluations(table)
# Ajoute style "col_empty" aux colonnes de modules vides
row_moy = table.get_row_by_id("moy")
for col_id in table.column_ids:
if "col_empty" in row_moy.cells[col_id]:
table.column_classes[col_id].append("col_empty")
# Ligne avec la classe de chaque colonne
# récupère le type à partir des classes css (hack...)
row_type = tb.BottomRow(
table,
"type_col",
title="Type col.",
left_title_col_ids=["prenom", "nom_short"],
category="bottom_infos",
classes=["bottom_info"],
)
for col_id in table.column_ids:
group_name = table.column_group.get(col_id, "")
if group_name.startswith("col_"):
group_name = group_name[4:]
row_type.add_cell(col_id, None, group_name)
# Titres
table.add_head_row(self.head_title_row)
table.add_foot_row(self.foot_title_row)
return table
def _recap_bottom_infos(
self, table: tb.Table, ues, modimpl_ids: set, fmt_note
) -> dict:
"""Les informations à mettre en bas de la table: min, max, moy, ECTS, Apo"""
# Ordre des lignes: Min, Max, Moy, Coef, ECTS, Apo
row_min = tb.BottomRow(
table,
"min",
title="Min.",
left_title_col_ids=["prenom", "nom_short"],
category="bottom_infos",
classes=["bottom_info"],
)
row_max = tb.BottomRow(
table,
"max",
title="Max.",
left_title_col_ids=["prenom", "nom_short"],
category="bottom_infos",
classes=["bottom_info"],
)
row_moy = tb.BottomRow(
table,
"moy",
title="Moy.",
left_title_col_ids=["prenom", "nom_short"],
category="bottom_infos",
classes=["bottom_info"],
)
row_coef = tb.BottomRow(
table,
"coef",
title="Coef.",
left_title_col_ids=["prenom", "nom_short"],
category="bottom_infos",
classes=["bottom_info"],
)
row_ects = tb.BottomRow(
table,
"ects",
title="ECTS",
left_title_col_ids=["prenom", "nom_short"],
category="bottom_infos",
classes=["bottom_info"],
)
row_apo = tb.BottomRow(
table,
"apo",
title="Code Apogée",
left_title_col_ids=["prenom", "nom_short"],
category="bottom_infos",
classes=["bottom_info"],
)
# --- ECTS
# titre (à gauche) sur 2 colonnes pour s'adapter à l'affichage des noms/prenoms
for ue in ues:
col_id = f"moy_ue_{ue.id}"
row_ects.add_cell(col_id, None, ue.ects, "col_ue")
# ajoute cell UE vides sur ligne coef pour borders verticales
# XXX TODO classes dans table sur colonne ajoutées à tous les TD
row_coef.add_cell(col_id, None, "", "col_ue")
row_ects.add_cell(
"moy_gen",
None,
sum([ue.ects or 0 for ue in ues if ue.type != UE_SPORT]),
"col_moy_gen",
)
# --- MIN, MAX, MOY, APO
row_min.add_cell("moy_gen", None, fmt_note(self.etud_moy_gen.min()))
row_max.add_cell("moy_gen", None, fmt_note(self.etud_moy_gen.max()))
row_moy.add_cell("moy_gen", None, fmt_note(self.etud_moy_gen.mean()))
for ue in ues:
col_id = f"moy_ue_{ue.id}"
row_min.add_cell(col_id, None, fmt_note(self.etud_moy_ue[ue.id].min()))
row_max.add_cell(col_id, None, fmt_note(self.etud_moy_ue[ue.id].max()))
row_moy.add_cell(col_id, None, fmt_note(self.etud_moy_ue[ue.id].mean()))
row_apo.add_cell(col_id, None, ue.code_apogee or "")
for modimpl in self.formsemestre.modimpls_sorted:
if modimpl.id in modimpl_ids:
col_id = f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}"
if self.is_apc:
coef = self.modimpl_coefs_df[modimpl.id][ue.id]
else:
coef = modimpl.module.coefficient or 0
row_coef.add_cell(col_id, None, fmt_note(coef))
notes = self.modimpl_notes(modimpl.id, ue.id)
if np.isnan(notes).all():
# aucune note valide
row_min.add_cell(col_id, None, np.nan)
row_max.add_cell(col_id, None, np.nan)
moy = np.nan
else:
row_min.add_cell(col_id, None, fmt_note(np.nanmin(notes)))
row_max.add_cell(col_id, None, fmt_note(np.nanmax(notes)))
moy = np.nanmean(notes)
row_moy.add_cell(
col_id,
None,
fmt_note(moy),
# aucune note dans ce module ?
classes=["col_empty" if np.isnan(moy) else ""],
)
row_apo.add_cell(col_id, None, modimpl.module.code_apogee or "")
return { # { key : row } avec key = min, max, moy, coef, ...
"min": row_min,
"max": row_max,
"moy": row_moy,
"coef": row_coef,
"ects": row_ects,
"apo": row_apo,
}
def _recap_etud_groups_infos( def _recap_etud_groups_infos(
self, etudid: int, row: dict, titles: dict self, etudid: int, row: dict, titles: dict
@ -1034,13 +1038,13 @@ class RowRecap(tb.Row):
Les colonnes ont la classe css "partition" Les colonnes ont la classe css "partition"
""" """
table.insert_group("partition", after="identite_court") table.insert_group("partition", after="parcours")
partitions, partitions_etud_groups = sco_groups.get_formsemestre_groups( partitions, partitions_etud_groups = sco_groups.get_formsemestre_groups(
self.formsemestre.id self.formsemestre.id
) )
first_partition = True first_partition = True
for partition in partitions: for partition in partitions:
col_classes = [] # la classe "partition" sera ajoutée par la table col_classes = ["partition"]
if not first_partition: if not first_partition:
col_classes.append("partition_aux") col_classes.append("partition_aux")
first_partition = False first_partition = False
@ -1059,7 +1063,6 @@ class RowRecap(tb.Row):
partition_etud_groups = partitions_etud_groups[partition["partition_id"]] partition_etud_groups = partitions_etud_groups[partition["partition_id"]]
for row in table.rows: for row in table.rows:
etudid = row.id
group = None # group (dict) de l'étudiant dans cette partition group = None # group (dict) de l'étudiant dans cette partition
# dans NotesTableCompat, à revoir # dans NotesTableCompat, à revoir
etud_etat = self.get_etud_etat(row.id) # row.id == etudid etud_etat = self.get_etud_etat(row.id) # row.id == etudid
@ -1089,7 +1092,7 @@ class RowRecap(tb.Row):
and (group["id"] in self.moy_gen_rangs_by_group) and (group["id"] in self.moy_gen_rangs_by_group)
): ):
rang = self.moy_gen_rangs_by_group[group["id"]][0] rang = self.moy_gen_rangs_by_group[group["id"]][0]
row.add_cell(rg_cid, None, rang.get(etudid, ""), "partition") row.add_cell(rg_cid, None, rang.get(row["etudid"], ""), "partition")
def _recap_add_evaluations(self, table: tb.Table): def _recap_add_evaluations(self, table: tb.Table):
"""Ajoute les colonnes avec les notes aux évaluations """Ajoute les colonnes avec les notes aux évaluations
@ -1100,7 +1103,7 @@ class RowRecap(tb.Row):
row_descr_eval = tb.BottomRow( row_descr_eval = tb.BottomRow(
table, table,
"evaluations", "evaluations",
left_title="Description évaluations", title="Description évaluations",
left_title_col_ids=["prenom", "nom_short"], left_title_col_ids=["prenom", "nom_short"],
category="bottom_infos", category="bottom_infos",
classes=["bottom_info"], classes=["bottom_info"],
@ -1133,7 +1136,7 @@ class RowRecap(tb.Row):
else: else:
# 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
content = table.fmt_note(val) content = scu.fmt_note(val)
classes = col_classes + [ classes = col_classes + [
{ {
"ABS": "abs", "ABS": "abs",
@ -1153,7 +1156,7 @@ class RowRecap(tb.Row):
table.get_row_by_id("coef").row[col_id] = e.coefficient table.get_row_by_id("coef").row[col_id] = e.coefficient
table.get_row_by_id("min").row[col_id] = "0" table.get_row_by_id("min").row[col_id] = "0"
table.get_row_by_id("max").row[col_id] = table.fmt_note(e.note_max) table.get_row_by_id("max").row[col_id] = scu.fmt_note(e.note_max)
row_descr_eval.add_cell( row_descr_eval.add_cell(
col_id, col_id,
None, None,

View File

@ -488,9 +488,54 @@ def _gen_formsemestre_recapcomplet_table(
mode_jury=mode_jury, mode_jury=mode_jury,
read_only=not formsemestre.can_edit_jury(), read_only=not formsemestre.can_edit_jury(),
) )
table.data["filename"] = filename if not rows:
table.select_row(selected_etudid) return (
return table '<div class="table_recap"><div class="message">aucun étudiant !</div></div>'
)
H = [
f"""<div class="table_recap">
{
table.html(
classes=[
'table_recap',
'apc' if formsemestre.formation.is_apc() else 'classic',
'jury' if mode_jury else ''
],
data={"filename":filename}
)
}
<table class="table_recap {
'apc' if formsemestre.formation.is_apc() else 'classic'
} {'jury' if mode_jury else ''}"
data-filename="{filename}">
"""
]
# header
H.append(
f"""
<thead>
{scu.gen_row(column_ids, titles, "th")}
</thead>
"""
)
# body
H.append("<tbody>")
for row in rows:
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'{scu.gen_row(column_ids, row, "th" if i == idx_last else "td")}\n')
H.append(
"""
</tfoot>
</table>
</div>
"""
)
return "".join(H)
def gen_formsemestre_recapcomplet_excel( def gen_formsemestre_recapcomplet_excel(

View File

@ -9,39 +9,7 @@
from collections import defaultdict from collections import defaultdict
class Element: class Table:
def __init__(
self,
elt: str,
content=None,
classes: list[str] = None,
attrs: dict[str, str] = None,
data: dict = None,
):
self.elt = elt
self.attrs = attrs or {}
self.classes = classes or []
"list of classes for the element"
self.content = content
self.data = data or {}
"data-xxx"
def html(self, extra_classes: list[str] = None) -> str:
"html for element"
classes = [cls for cls in (self.classes + (extra_classes or [])) if cls]
attrs_str = f"""class="{' '.join(classes)}" """ if classes else ""
# Autres attributs:
attrs_str += " " + " ".join([f'{k}="{v}"' for (k, v) in self.attrs.items()])
# et data-x
attrs_str += " " + " ".join([f'data-{k}="{v}"' for k, v in self.data.items()])
return f"""<{self.elt} {attrs_str}>{self.html_content()}</{self.elt}>"""
def html_content(self) -> str:
"Le contenu de l'élément, en html."
return str(self.content or "")
class Table(Element):
"""Construction d'une table de résultats """Construction d'une table de résultats
table = Table() table = Table()
@ -54,33 +22,33 @@ class Table(Element):
table.update_titles(titles) table.update_titles(titles)
table.set_column_groups(groups: list[str]) table.set_column_groups(groups: list[str])
table.sort_columns()
table.insert_group(group:str, [after=str], [before=str]) table.insert_group(group:str, [after=str], [before=str])
Ordre des colonnes: groupées par groupes, et dans chaque groupe par ordre d'insertion
On fixe l'ordre des groupes par ordre d'insertion
ou par insert_group ou par set_column_groups.
""" """
def __init__( def __init__(
self, self,
selected_row_id: str = None, selected_row_id: str = None,
classes: list[str] = None, classes: list[str] = None,
attrs: dict[str, str] = None, data: dict[str, str] = None,
data: dict = None,
): ):
super().__init__("table", classes=classes, attrs=attrs, data=data)
self.rows: list["Row"] = [] self.rows: list["Row"] = []
"ordered list of Rows" "ordered list of Rows"
self.row_by_id: dict[str, "Row"] = {} self.row_by_id: dict[str, "Row"] = {}
self.classes = classes or []
"list of classes for the table element"
self.column_ids = [] self.column_ids = []
"ordered list of columns ids" "ordered list of columns ids"
self.data = data or {}
"data-xxx"
self.groups = [] self.groups = []
"ordered list of column groups names" "ordered list of column groups names"
self.head = [] self.head = []
self.foot = [] self.foot = []
self.column_group = {} self.column_group = {}
"the group of the column: { col_id : group }" "the group of the column: { col_id : group }"
self.column_index: dict[str, int] = {}
"index the column: { col_id : int }"
self.column_classes: defaultdict[str, list[str]] = defaultdict(lambda: []) self.column_classes: defaultdict[str, list[str]] = defaultdict(lambda: [])
"classe ajoutée à toutes les cellules de la colonne: { col_id : class }" "classe ajoutée à toutes les cellules de la colonne: { col_id : class }"
self.selected_row_id = selected_row_id self.selected_row_id = selected_row_id
@ -91,39 +59,24 @@ class Table(Element):
self.foot_title_row: "Row" = Row(self, "title_foot", cell_elt="th") self.foot_title_row: "Row" = Row(self, "title_foot", cell_elt="th")
self.empty_cell = Cell.empty() self.empty_cell = Cell.empty()
def _prepare(self):
"""Prepare the table before generation:
Sort table columns, add header/footer titles rows
"""
self.sort_columns()
# Titres
self.add_head_row(self.head_title_row)
self.add_foot_row(self.foot_title_row)
def get_row_by_id(self, row_id) -> "Row": def get_row_by_id(self, row_id) -> "Row":
"return the row, or None" "return the row, or None"
return self.row_by_id.get(row_id) return self.row_by_id.get(row_id)
def is_empty(self) -> bool: def _prepare(self):
"true if table has no rows" """Sort table elements before generation"""
return len(self.rows) == 0 self.sort_columns()
def select_row(self, row_id): def html(self, classes: list[str] = None, data: dict[str, str] = None) -> str:
"mark rows as 'selected'" """HTML version of the table
self.selected_row_id = row_id classes are prepended to existing classes
data may replace existing data
def to_list(self) -> list[dict]: """
"""as a list, each row is a dict"""
self._prepare() self._prepare()
return [row.to_dict() for row in self.rows] classes = classes + self.classes
data = self.data.copy().update(data)
def html(self, extra_classes: list[str] = None) -> str: elt_class = f"""class="{' '.join(classes)}" """ if classes else ""
"""HTML version of the table""" attrs_str = " ".join(f' data-{k}="v"' for k, v in data.items())
self._prepare()
return super().html(extra_classes=extra_classes)
def html_content(self) -> str:
"""Le contenu de la table en html."""
newline = "\n" newline = "\n"
header = ( header = (
f""" f"""
@ -143,14 +96,13 @@ class Table(Element):
if self.foot if self.foot
else "" else ""
) )
return f""" return f"""<table {elt_class} {attrs_str}>
{header} {header}
<tbody>
{ {
newline.join(row.html() for row in self.rows) newline.join(row.html() for row in self.rows)
} }
</tbody>
{footer} {footer}
</table>
""" """
def add_row(self, row: "Row") -> "Row": def add_row(self, row: "Row") -> "Row":
@ -179,12 +131,8 @@ class Table(Element):
def sort_columns(self): def sort_columns(self):
"""Sort columns ids""" """Sort columns ids"""
groups_order = {group: i for i, group in enumerate(self.groups)} groups_order = {group: i for i, group in enumerate(self.groups)}
cols_order = {col_id: i for i, col_id in enumerate(self.column_ids)}
self.column_ids.sort( self.column_ids.sort(
key=lambda col_id: ( key=lambda col_id: (groups_order.get(self.column_group.get(col_id), col_id))
groups_order.get(self.column_group.get(col_id), col_id),
cols_order[col_id],
)
) )
def insert_group(self, group: str, after: str = None, before: str = None): def insert_group(self, group: str, after: str = None, before: str = None):
@ -217,26 +165,23 @@ class Table(Element):
"""Set columns titles""" """Set columns titles"""
self.titles.update(titles) self.titles.update(titles)
def add_title( def add_title(self, col_id, title: str = None) -> tuple["Cell", "Cell"]:
self, col_id, title: str = None, classes: list[str] = None
) -> tuple["Cell", "Cell"]:
"""Record this title, """Record this title,
and create cells for footer and header if they don't already exist. and create cells for footer and header if they don't already exist.
""" """
title = title or "" title = title or ""
if col_id not in self.titles: if not col_id in self.titles:
self.titles[col_id] = title self.titles[col_id] = title
self.head_title_row.cells[col_id] = self.head_title_row.add_cell( cell_head = self.head_title_row.cells.get(
col_id, None, title, classes=classes col_id, self.head_title_row.add_cell(col_id, title, title)
) )
self.foot_title_row.cells[col_id] = self.foot_title_row.add_cell( cell_foot = self.foot_title_row.cells.get(
col_id, None, title, classes=classes col_id, self.foot_title_row.add_cell(col_id, title, title)
) )
return cell_head, cell_foot
return self.head_title_row.cells.get(col_id), self.foot_title_row.cells[col_id]
class Row(Element): class Row:
"""A row.""" """A row."""
def __init__( def __init__(
@ -246,15 +191,13 @@ class Row(Element):
category=None, category=None,
cell_elt: str = None, cell_elt: str = None,
classes: list[str] = None, classes: list[str] = None,
attrs: dict[str, str] = None,
data: dict = None,
): ):
super().__init__("tr", classes=classes, attrs=attrs, data=data)
self.category = category self.category = category
self.cells = {} self.cells = {}
self.cell_elt = cell_elt self.cell_elt = cell_elt
self.classes: list[str] = classes or [] self.classes: list[str] = classes or []
"classes sur le <tr>" "classes sur le <tr>"
self.cur_idx_by_group = defaultdict(lambda: 0)
self.id = row_id self.id = row_id
self.table = table self.table = table
@ -268,17 +211,23 @@ class Row(Element):
classes: list[str] = None, classes: list[str] = None,
data: dict[str, str] = None, data: dict[str, str] = None,
elt: str = None, elt: str = None,
idx: int = None,
raw_content=None, raw_content=None,
target_attrs: dict = None, target_attrs: dict = None,
target: str = None, target: str = None,
) -> "Cell": ) -> "Cell":
"""Create cell and add it to the row. """Create cell and add it to the row.
group: groupe de colonnes
classes is a list of css class names classes is a list of css class names
""" """
if idx is None:
idx = self.cur_idx_by_group[group]
self.cur_idx_by_group[group] += 1
else:
self.cur_idx_by_group[group] = idx
self.table.column_index[col_id] = idx
cell = Cell( cell = Cell(
content, content,
(classes or []) + [group or ""], # ajoute le nom de groupe aux classes classes + [group or ""], # ajoute le nom de groupe aux classes
elt=elt or self.cell_elt, elt=elt or self.cell_elt,
attrs=attrs, attrs=attrs,
data=data, data=data,
@ -294,42 +243,28 @@ class Row(Element):
"""Add a cell to the row. """Add a cell to the row.
Si title est None, il doit avoir été ajouté avec table.add_title(). Si title est None, il doit avoir été ajouté avec table.add_title().
""" """
cell.data["group"] = column_group
self.cells[col_id] = cell self.cells[col_id] = cell
if col_id not in self.table.column_ids:
self.table.column_ids.append(col_id)
self.table.insert_group(column_group)
if column_group is not None: if column_group is not None:
self.table.column_group[col_id] = column_group self.table.column_group[col_id] = column_group
if title is not None: if title is not None:
self.table.add_title(col_id, title, classes=cell.classes) self.table.add_title(col_id, title)
return cell return cell
def html(self, extra_classes: list[str] = None) -> str: def html(self) -> str:
"""html for row, with cells""" """html for row, with cells"""
if (self.id is not None) and self.id == getattr(self.table, "selected_row_id"): elt_class = f"""class="{' '.join(self.classes)}" """ if self.classes else ""
self.classes.append("row_selected") tr_id = (
return super().html(extra_classes=extra_classes) """id="row_selected" """ if (self.id == self.table.selected_etudid) else ""
) # TODO XXX remplacer id par une classe
def html_content(self) -> str: return f"""<tr {tr_id} {elt_class}>{
"Le contenu du row en html." "".join([self.cells.get(col_id,
return "".join( self.table.empty_cell).html(
[ column_classes=self.table.column_classes.get(col_id)
self.cells.get(col_id, self.table.empty_cell).html(
extra_classes=self.table.column_classes.get(col_id)
) )
for col_id in self.table.column_ids for col_id in self.table.column_ids ])
] }</tr>"""
)
def to_dict(self) -> dict:
"""row as a dict, with only cell contents"""
return {
col_id: self.cells.get(col_id, self.table.empty_cell).raw_content
for col_id in self.table.column_ids
}
class BottomRow(Row): class BottomRow(Row):
@ -354,7 +289,7 @@ class BottomRow(Row):
self.add_cell(col_id, None, title) self.add_cell(col_id, None, title)
class Cell(Element): class Cell:
"""Une cellule de table""" """Une cellule de table"""
def __init__( def __init__(
@ -362,21 +297,21 @@ class Cell(Element):
content, content,
classes: list[str] = None, classes: list[str] = None,
elt="td", elt="td",
attrs: dict[str, str] = None, attrs: list[str] = None,
data: dict = None, data: dict = None,
raw_content=None, raw_content=None,
target: str = None, target: str = None,
target_attrs: dict = None, target_attrs: dict = None,
): ):
"""if specified, raw_content will be used for raw exports like xlsx""" """if specified, raw_content will be used for raw exports like xlsx"""
super().__init__( self.content = content
elt if elt is not None else "td", content, classes, attrs, data self.classes: list = classes or []
) self.elt = elt if elt is not None else "td"
self.attrs = attrs or []
if self.elt == "th": if self.elt == "th":
self.attrs["scope"] = "row" self.attrs["scope"] = "row"
self.data = data or {} self.data = data or {}
self.raw_content = raw_content or content self.raw_content = raw_content # not yet used
self.target = target self.target = target
self.target_attrs = target_attrs or {} self.target_attrs = target_attrs or {}
@ -388,14 +323,20 @@ class Cell(Element):
def __str__(self): def __str__(self):
return str(self.content) return str(self.content)
def html_content(self) -> str: def html(self, column_classes: list[str] = None) -> str:
"content of the table cell, as html" "html for cell"
# entoure le contenu par un lien ? attrs_str = f"""class="{' '.join(
[cls for cls in (self.classes + (column_classes or [])) if cls])
}" """
# Autres attributs:
attrs_str += " ".join([f"{k}={v}" for (k, v) in self.attrs.items()])
# et data-x
attrs_str += " ".join([f' data-{k}="v"' for k, v in self.data.items()])
if (self.target is not None) or self.target_attrs: if (self.target is not None) or self.target_attrs:
href = f'href="{self.target}"' if self.target else "" href = f'href="{self.target}"' if self.target else ""
target_attrs_str = " ".join( target_attrs_str = " ".join(
[f'{k}="{v}"' for (k, v) in self.target_attrs.items()] [f"{k}={v}" for (k, v) in self.target_attrs.items()]
) )
return f"<a {href} {target_attrs_str}>{super().html_content()}</a>" content = f"<a {href} {target_attrs_str}>{content}</a>"
return f"""<{self.elt} {attrs_str}>{self.content}</{self.elt}>"""
return super().html_content()