WIP: table recap

This commit is contained in:
Emmanuel Viennet 2023-01-29 17:52:39 -03:00
parent 8af28d2f36
commit d2923f090c
7 changed files with 487 additions and 459 deletions

View File

@ -476,16 +476,17 @@ def formsemestre_resultat(formsemestre_id: int):
formsemestre: FormSemestre = query.first_or_404(formsemestre_id) formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
app.set_sco_dept(formsemestre.departement.acronym) app.set_sco_dept(formsemestre.departement.acronym)
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
rows, footer_rows, titles, column_ids = res.get_table_recap( table = res.get_table_recap(
convert_values=convert_values, convert_values=convert_values,
include_evaluations=False, include_evaluations=False,
mode_jury=False, mode_jury=False,
allow_html=False, allow_html=False,
) )
# Supprime les champs inutiles (mise en forme) # Supprime les champs inutiles (mise en forme)
table = [{k: row[k] for k in row if not k[0] == "_"} for row in rows] rows = table.to_list()
# Ajoute les groupes # Ajoute le groupe de chaque partition:
etud_groups = sco_groups.get_formsemestre_etuds_groups(formsemestre_id) etud_groups = sco_groups.get_formsemestre_etuds_groups(formsemestre_id)
for row in table: for row in rows:
row["partitions"] = etud_groups.get(row["etudid"], {}) row["partitions"] = etud_groups.get(row["etudid"], {})
return jsonify(table)
return jsonify(rows)

View File

@ -440,15 +440,12 @@ class ResultatsSemestre(ResultatsCache):
convert_values=False, convert_values=False,
include_evaluations=False, include_evaluations=False,
mode_jury=False, mode_jury=False,
allow_html=True,
) -> tb.Table: ) -> tb.Table:
"""Table récap. des résultats. """Table récap. des résultats.
allow_html: si vrai, peut mettre du HTML dans les valeurs allow_html: si vrai, peut mettre du HTML dans les valeurs
Result: XXX tuple avec Result: Table, le contenu étant une ligne par étudiant.
- 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"). Si convert_values, transforme les notes en chaines ("12.34").
Les colonnes générées sont: Les colonnes générées sont:
@ -459,83 +456,113 @@ class ResultatsSemestre(ResultatsCache):
moy_res_<modimpl_id>_<ue_id>, ... les moyennes de ressources dans l'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 moy_sae_<modimpl_id>_<ue_id>, ... les moyennes de SAE dans l'UE
On ajoute aussi des attributs: XXX On ajoute aussi des classes:
- pour les lignes: - pour les lignes:
_css_row_class (inutilisé pour le monent) selected_row pour l'étudiant sélectionné
_<column_id>_class classe css: - les colonnes:
- la moyenne générale a la classe col_moy_gen - la moyenne générale a la classe col_moy_gen
- les colonnes SAE ont la classe col_sae - les colonnes SAE ont la classe col_sae
- les colonnes Resources ont la classe col_res - les colonnes Resources ont la classe col_res
- les colonnes d'UE ont la classe col_ue - 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> - 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()
barre_moy = parcours.BARRE_MOY - scu.NOTES_TOLERANCE
barre_valid_ue = parcours.NOTES_BARRE_VALID_UE
barre_warning_ue = parcours.BARRE_UE_DISPLAY_WARNING
NO_NOTE = "-" # contenu des cellules sans notes
rows = []
dict_nom_res = {} # cache uid : nomcomplet
# def add_cell(
# row: dict,
# 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() table = tb.Table()
# Quelques infos stockées dans la Table
parcours = self.formsemestre.formation.get_parcours()
table.barre_moy = parcours.BARRE_MOY - scu.NOTES_TOLERANCE
table.barre_valid_ue = parcours.NOTES_BARRE_VALID_UE
table.barre_warning_ue = parcours.BARRE_UE_DISPLAY_WARNING
table.cache_nomcomplet = {} # cache uid : nomcomplet
if convert_values:
table.fmt_note = scu.fmt_note
else:
table.fmt_note = lambda x: x
# couples (modimpl, ue) effectivement présents dans la table:
table.modimpl_ue_ids = set()
for etudid in etuds_inscriptions: for etudid in etuds_inscriptions:
idx = 0 # index de la colonne
etud = Identite.query.get(etudid) etud = Identite.query.get(etudid)
row = tb.Row(table, etudid) row = tb.Row(table, etudid)
table.add_row(row) table.add_row(row)
self._recap_add_etud(row, etud)
self._recap_add_moyennes(row, etud, ues_sans_bonus, mode_jury)
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 row: row.rang_order)
else:
table.sort_rows(key=lambda row: row.nb_ues_validables, reverse=True)
# Lignes footer (min, max, ects, apo, ...)
self._recap_add_bottom_rows(table, ues_sans_bonus)
# Evaluations:
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:
cell: tb.Cell = row_moy.cells.get(col_id)
if cell and "col_empty" in cell.classes:
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",
left_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)
return table
def _recap_add_etud(self, row: tb.Row, etud: Identite):
"""Ajoute colonnes étudiant: codes, noms"""
# --- Codes (seront cachés, mais exportés en excel) # --- Codes (seront cachés, mais exportés en excel)
row.add_cell("etudid", "etudid", etudid, "codes") row.add_cell("etudid", "etudid", etud.id, "etud_codes")
row.add_cell( row.add_cell(
"code_nip", "code_nip",
"code_nip", "code_nip",
etud.code_nip or "", etud.code_nip or "",
"codes", "etud_codes",
) )
# --- Rang # --- Rang
if not self.formsemestre.block_moyenne_generale: if not self.formsemestre.block_moyenne_generale:
row.rang_order = self.etud_moy_gen_ranks_int[etud.id]
row.add_cell( row.add_cell(
"rang", "rang",
"Rg", "Rg",
self.etud_moy_gen_ranks[etudid], self.etud_moy_gen_ranks[etud.id],
"rang", "rang",
data={"order": f"{self.etud_moy_gen_ranks_int[etudid]:05d}"}, data={"order": f"{row.rang_order:05d}"},
) )
else:
row.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=self.formsemestre.id, formsemestre_id=self.formsemestre.id,
etudid=etudid, etudid=etud.id,
) )
row.add_cell("civilite_str", "Civ.", etud.civilite_str, "identite_detail") row.add_cell("civilite_str", "Civ.", etud.civilite_str, "identite_detail")
row.add_cell( row.add_cell(
@ -545,7 +572,7 @@ class ResultatsSemestre(ResultatsCache):
"identite_detail", "identite_detail",
data={"order": etud.sort_key}, data={"order": etud.sort_key},
target=url_bulletin, target=url_bulletin,
target_attrs=f'class="etudinfo" id="{etudid}"', target_attrs={"class": "etudinfo", "id": str(etud.id)},
) )
row.add_cell("prenom", "Prénom", etud.prenom, "identite_detail") row.add_cell("prenom", "Prénom", etud.prenom, "identite_detail")
row.add_cell( row.add_cell(
@ -559,65 +586,55 @@ class ResultatsSemestre(ResultatsCache):
"nomprenom": etud.nomprenom, "nomprenom": etud.nomprenom,
}, },
target=url_bulletin, target=url_bulletin,
target_attrs=f'class="etudinfo" id="{etudid}"', target_attrs={"class": "etudinfo", "id": str(etud.id)},
) )
def _recap_add_moyennes(
self,
row: tb.Row,
etud: Identite,
ues_sans_bonus: list[UniteEns],
mode_jury=False,
):
"""Ajoute cols moy_gen moy_ue et tous les modules..."""
table = row.table
# --- Moyenne générale # --- Moyenne générale
if not self.formsemestre.block_moyenne_generale: if not self.formsemestre.block_moyenne_generale:
moy_gen = self.etud_moy_gen.get(etudid, False) moy_gen = self.etud_moy_gen.get(etud.id, False)
note_class = "" note_class = ""
if moy_gen is False: if moy_gen is False:
moy_gen = NO_NOTE moy_gen = scu.NO_NOTE_STR
elif isinstance(moy_gen, float) and moy_gen < barre_moy: elif isinstance(moy_gen, float) and moy_gen < table.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",
fmt_note(moy_gen), table.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:
table.foot_title_row.cells["moy_gen"].target_attrs = ( if self.is_apc:
'title="moyenne indicative"' if self.is_apc else "" table.foot_title_row.cells["moy_gen"].target_attrs[
) "title"
] = "moyenne indicative"
# --- Moyenne d'UE # --- Moyenne d'UE
nb_ues_validables, nb_ues_warning = 0, 0 row.nb_ues_validables, row.nb_ues_warning = 0, 0
for ue in ues_sans_bonus: for ue in ues_sans_bonus:
ue_status = self.get_etud_ue_status(etudid, ue.id) ue_status = self.get_etud_ue_status(etud.id, ue.id)
if ue_status is not None: if ue_status is not None:
col_id = f"moy_ue_{ue.id}" self._recap_add_ue(row, ue, ue_status)
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: 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 (self.bonus_ues is not None) and (ue.id in self.bonus_ues): if (self.bonus_ues is not None) and (ue.id in self.bonus_ues):
val = self.bonus_ues[ue.id][etud.id] or "" val = self.bonus_ues[ue.id][etud.id] or ""
val_fmt = val_fmt_html = fmt_note(val) val_fmt = val_fmt_html = table.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
@ -625,54 +642,126 @@ class ResultatsSemestre(ResultatsCache):
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 if allow_html else val_fmt, val_fmt_html,
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
for modimpl in self.modimpls_in_ue(ue, etudid, with_bonus=False): self._recap_add_ue_modimpls(row, ue, etud, ue_status["is_capitalized"])
if ue_status["is_capitalized"]:
row.nb_ues_etud_parcours = len(self.etud_ues_ids(etud.id))
ue_valid_txt = (
ue_valid_txt_html
) = f"{row.nb_ues_validables}/{row.nb_ues_etud_parcours}"
if row.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 row.nb_ues_warning:
classes.append("moy_ue_warning")
elif row.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": row.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(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 = self.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, "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=etud.id
)
}">{("saisir" if not jury_code_sem else "modifier")
if self.formsemestre.etat else "voir"} décisions</a>""",
"col_jury_link",
)
def _recap_add_ue(self, row: tb.Row, ue: UniteEns, ue_status: dict):
"Ajoute résultat UE au row (colonne col_ue)"
table = row.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"
row.nb_ues_validables += 1
if val < table.barre_warning_ue:
note_class = " moy_ue_warning" # notes très basses
row.nb_ues_warning += 1
row.add_cell(
col_id,
ue.acronyme,
table.fmt_note(val),
group=f"col_ue_{ue.id}",
classes=["col_ue", note_class],
)
row.table.foot_title_row.cells[col_id].target_attrs[
"title"
] = f"""{ue.titre} S{ue.semestre_idx or '?'}"""
def _recap_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"""
table = row.table
for modimpl in self.modimpls_in_ue(ue, etud.id, with_bonus=False):
if is_capitalized:
val = "-c-" val = "-c-"
else: else:
modimpl_results = self.modimpls_results.get(modimpl.id) modimpl_results = self.modimpls_results.get(modimpl.id)
if modimpl_results: # pas bonus if modimpl_results: # pas bonus
if self.is_apc: # BUT if self.is_apc: # BUT
moys_vers_ue = modimpl_results.etuds_moy_module.get( moys_vers_ue = modimpl_results.etuds_moy_module.get(ue.id)
ue.id
)
val = ( val = (
moys_vers_ue.get(etudid, "?") moys_vers_ue.get(etud.id, "?")
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( val = modimpl_results.etuds_moy_module.get(etud.id, "?")
etudid, "?"
)
else: else:
val = "" val = ""
col_id = ( col_id = f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}"
f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}" val_fmt = val_fmt_html = table.fmt_note(val)
) if modimpl.module.module_type == scu.ModuleType.MALUS:
val_fmt = val_fmt_html = fmt_note(val) val_fmt_html = (scu.EMO_RED_TRIANGLE_DOWN + val_fmt) if val else ""
if convert_values and (
modimpl.module.module_type == scu.ModuleType.MALUS
):
val_fmt_html = (
(scu.EMO_RED_TRIANGLE_DOWN + val_fmt) if val else ""
)
cell = row.add_cell( cell = row.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
@ -685,119 +774,23 @@ class ResultatsSemestre(ResultatsCache):
moduleimpl_id=modimpl.id, moduleimpl_id=modimpl.id,
) )
nom_resp = dict_nom_res.get(modimpl.responsable_id) nom_resp = table.cache_nomcomplet.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 ""
dict_nom_res[modimpl.responsable_id] = nom_resp table.cache_nomcomplet[modimpl.responsable_id] = nom_resp
table.foot_title_row.cells[ table.foot_title_row.cells[col_id].target_attrs[
col_id "title"
].target_attrs = ( ] = f"{modimpl.module.titre} ({nom_resp})"
f""" title="{modimpl.module.titre} ({nom_resp})" """ table.modimpl_ue_ids.add((modimpl.id, ue.id))
)
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: def _recap_add_bottom_rows(self, table: tb.Table, ues):
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")
if self.formsemestre.etat else "voir"} décisions</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""" """Les informations à mettre en bas de la table: min, max, moy, ECTS, Apo"""
# Ordre des lignes: Min, Max, Moy, Coef, ECTS, Apo # Ordre des lignes: Min, Max, Moy, Coef, ECTS, Apo
row_min = tb.BottomRow( row_min = tb.BottomRow(
table, table,
"min", "min",
title="Min.", left_title="Min.",
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"],
@ -805,7 +798,7 @@ class ResultatsSemestre(ResultatsCache):
row_max = tb.BottomRow( row_max = tb.BottomRow(
table, table,
"max", "max",
title="Max.", left_title="Max.",
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"],
@ -813,7 +806,7 @@ class ResultatsSemestre(ResultatsCache):
row_moy = tb.BottomRow( row_moy = tb.BottomRow(
table, table,
"moy", "moy",
title="Moy.", left_title="Moy.",
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"],
@ -821,7 +814,7 @@ class ResultatsSemestre(ResultatsCache):
row_coef = tb.BottomRow( row_coef = tb.BottomRow(
table, table,
"coef", "coef",
title="Coef.", left_title="Coef.",
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"],
@ -829,7 +822,7 @@ class ResultatsSemestre(ResultatsCache):
row_ects = tb.BottomRow( row_ects = tb.BottomRow(
table, table,
"ects", "ects",
title="ECTS", left_title="ECTS",
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"],
@ -837,7 +830,7 @@ class ResultatsSemestre(ResultatsCache):
row_apo = tb.BottomRow( row_apo = tb.BottomRow(
table, table,
"apo", "apo",
title="Code Apogée", left_title="Code Apogée",
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"],
@ -847,36 +840,46 @@ class ResultatsSemestre(ResultatsCache):
# titre (à gauche) sur 2 colonnes pour s'adapter à l'affichage des noms/prenoms # titre (à gauche) sur 2 colonnes pour s'adapter à l'affichage des noms/prenoms
for ue in ues: for ue in ues:
col_id = f"moy_ue_{ue.id}" col_id = f"moy_ue_{ue.id}"
row_ects.add_cell(col_id, None, ue.ects, "col_ue") row_ects.add_cell(col_id, None, ue.ects)
# ajoute cell UE vides sur ligne coef pour borders verticales # ajoute cell UE vides sur ligne coef pour borders verticales
# XXX TODO classes dans table sur colonne ajoutées à tous les TD # XXX TODO classes dans table sur colonne ajoutées à tous les TD
row_coef.add_cell(col_id, None, "", "col_ue") row_coef.add_cell(col_id, None, "")
row_ects.add_cell( row_ects.add_cell(
"moy_gen", "moy_gen",
None, None,
sum([ue.ects or 0 for ue in ues if ue.type != UE_SPORT]), sum([ue.ects or 0 for ue in ues if ue.type != UE_SPORT]),
"col_moy_gen",
) )
# --- MIN, MAX, MOY, APO # --- MIN, MAX, MOY, APO
row_min.add_cell("moy_gen", None, fmt_note(self.etud_moy_gen.min())) row_min.add_cell("moy_gen", None, table.fmt_note(self.etud_moy_gen.min()))
row_max.add_cell("moy_gen", None, fmt_note(self.etud_moy_gen.max())) row_max.add_cell("moy_gen", None, table.fmt_note(self.etud_moy_gen.max()))
row_moy.add_cell("moy_gen", None, fmt_note(self.etud_moy_gen.mean())) row_moy.add_cell("moy_gen", None, table.fmt_note(self.etud_moy_gen.mean()))
for ue in ues: for ue in ues:
col_id = f"moy_ue_{ue.id}" col_id = f"moy_ue_{ue.id}"
row_min.add_cell(col_id, None, fmt_note(self.etud_moy_ue[ue.id].min())) row_min.add_cell(
row_max.add_cell(col_id, None, fmt_note(self.etud_moy_ue[ue.id].max())) col_id, None, table.fmt_note(self.etud_moy_ue[ue.id].min())
row_moy.add_cell(col_id, None, fmt_note(self.etud_moy_ue[ue.id].mean())) )
row_max.add_cell(
col_id, None, table.fmt_note(self.etud_moy_ue[ue.id].max())
)
row_moy.add_cell(
col_id, None, table.fmt_note(self.etud_moy_ue[ue.id].mean())
)
row_apo.add_cell(col_id, None, ue.code_apogee or "") row_apo.add_cell(col_id, None, ue.code_apogee or "")
for modimpl in self.formsemestre.modimpls_sorted: for modimpl in self.formsemestre.modimpls_sorted:
if modimpl.id in modimpl_ids: if (modimpl.id, ue.id) in table.modimpl_ue_ids:
col_id = f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}" col_id = f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}"
if self.is_apc: if self.is_apc:
coef = self.modimpl_coefs_df[modimpl.id][ue.id] coef = self.modimpl_coefs_df[modimpl.id][ue.id]
else: else:
coef = modimpl.module.coefficient or 0 coef = modimpl.module.coefficient or 0
row_coef.add_cell(col_id, None, fmt_note(coef)) row_coef.add_cell(
col_id,
None,
table.fmt_note(coef),
group=f"col_ue_{ue.id}_modules",
)
notes = self.modimpl_notes(modimpl.id, ue.id) notes = self.modimpl_notes(modimpl.id, ue.id)
if np.isnan(notes).all(): if np.isnan(notes).all():
# aucune note valide # aucune note valide
@ -884,27 +887,18 @@ class ResultatsSemestre(ResultatsCache):
row_max.add_cell(col_id, None, np.nan) row_max.add_cell(col_id, None, np.nan)
moy = np.nan moy = np.nan
else: else:
row_min.add_cell(col_id, None, fmt_note(np.nanmin(notes))) row_min.add_cell(col_id, None, table.fmt_note(np.nanmin(notes)))
row_max.add_cell(col_id, None, fmt_note(np.nanmax(notes))) row_max.add_cell(col_id, None, table.fmt_note(np.nanmax(notes)))
moy = np.nanmean(notes) moy = np.nanmean(notes)
row_moy.add_cell( row_moy.add_cell(
col_id, col_id,
None, None,
fmt_note(moy), table.fmt_note(moy),
# aucune note dans ce module ? # aucune note dans ce module ?
classes=["col_empty" if np.isnan(moy) else ""], classes=["col_empty" if np.isnan(moy) else ""],
) )
row_apo.add_cell(col_id, None, modimpl.module.code_apogee or "") 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
): # XXX non utilisé ): # XXX non utilisé
@ -992,13 +986,13 @@ class ResultatsSemestre(ResultatsCache):
Les colonnes ont la classe css "partition" Les colonnes ont la classe css "partition"
""" """
table.insert_group("partition", after="parcours") table.insert_group("partition", after="identite_court")
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 = ["partition"] col_classes = [] # la classe "partition" sera ajoutée par la table
if not first_partition: if not first_partition:
col_classes.append("partition_aux") col_classes.append("partition_aux")
first_partition = False first_partition = False
@ -1017,6 +1011,7 @@ class ResultatsSemestre(ResultatsCache):
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
@ -1028,7 +1023,7 @@ class ResultatsSemestre(ResultatsCache):
gr_name = "Déf." gr_name = "Déf."
tr_classes.append("def") tr_classes.append("def")
else: else:
group = partition_etud_groups.get(row["etudid"]) group = partition_etud_groups.get(etudid)
gr_name = group["group_name"] if group else "" gr_name = group["group_name"] if group else ""
if gr_name: if gr_name:
row.add_cell( row.add_cell(
@ -1046,7 +1041,7 @@ class ResultatsSemestre(ResultatsCache):
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(row["etudid"], ""), "partition") row.add_cell(rg_cid, None, rang.get(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
@ -1057,7 +1052,7 @@ class ResultatsSemestre(ResultatsCache):
row_descr_eval = tb.BottomRow( row_descr_eval = tb.BottomRow(
table, table,
"evaluations", "evaluations",
title="Description évaluations", left_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"],
@ -1090,7 +1085,7 @@ class ResultatsSemestre(ResultatsCache):
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 = scu.fmt_note(val) content = table.fmt_note(val)
classes = col_classes + [ classes = col_classes + [
{ {
"ABS": "abs", "ABS": "abs",
@ -1110,7 +1105,7 @@ class ResultatsSemestre(ResultatsCache):
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] = scu.fmt_note(e.note_max) table.get_row_by_id("max").row[col_id] = table.fmt_note(e.note_max)
row_descr_eval.add_cell( row_descr_eval.add_cell(
col_id, col_id,
None, None,

View File

@ -415,59 +415,27 @@ def _gen_formsemestre_recapcomplet_html(
selected_etudid=None, selected_etudid=None,
) -> str: ) -> str:
"""Génère le html""" """Génère le html"""
rows, footer_rows, titles, column_ids = res.get_table_recap( table = res.get_table_recap(
convert_values=True, convert_values=True,
include_evaluations=include_evaluations, include_evaluations=include_evaluations,
mode_jury=mode_jury, mode_jury=mode_jury,
) )
if not rows: table.data["filename"] = filename
return ( table.select_row(selected_etudid)
'<div class="table_recap"><div class="message">aucun étudiant !</div></div>' return f"""
) <div class="table_recap">
H = [
f"""<div class="table_recap">
{ {
table.html( '<div class="message">aucun étudiant !</div>'
classes=[ if table.is_empty()
else table.html(
extra_classes=[
'table_recap', 'table_recap',
'apc' if formsemestre.formation.is_apc() else 'classic', 'apc' if formsemestre.formation.is_apc() else 'classic',
'jury' if mode_jury else '' '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> </div>
""" """
)
return "".join(H)
def gen_formsemestre_recapcomplet_excel( def gen_formsemestre_recapcomplet_excel(

View File

@ -74,6 +74,8 @@ NOTES_NEUTRALISE = -1000.0 # notes non prises en comptes dans moyennes
NOTES_SUPPRESS = -1001.0 # note a supprimer NOTES_SUPPRESS = -1001.0 # note a supprimer
NOTES_ATTENTE = -1002.0 # note "en attente" (se calcule comme une note neutralisee) NOTES_ATTENTE = -1002.0 # note "en attente" (se calcule comme une note neutralisee)
NO_NOTE_STR = "-" # contenu des cellules de tableaux html sans notes
# ---- CODES INSCRIPTION AUX SEMESTRES # ---- CODES INSCRIPTION AUX SEMESTRES
# (champ etat de FormSemestreInscription) # (champ etat de FormSemestreInscription)
INSCRIT = "I" INSCRIT = "I"
@ -1178,11 +1180,10 @@ def gen_row(
): ):
"html table row" "html table row"
klass = row.get("_tr_class") klass = row.get("_tr_class")
if row.get("etudid", "") == selected_etudid:
klass += " row_selected"
tr_class = f'class="{klass}"' if klass else "" tr_class = f'class="{klass}"' if klass else ""
tr_id = ( return f"""<tr {tr_class}>{
f"""id="row_selected" """ if (row.get("etudid", "") == selected_etudid) else ""
)
return f"""<tr {tr_id} {tr_class}>{
"".join([gen_cell(key, row, elt, with_col_class=with_col_classes) "".join([gen_cell(key, row, elt, with_col_class=with_col_classes)
for key in keys if not key.startswith('_')]) for key in keys if not key.startswith('_')])
}</tr>""" }</tr>"""

View File

@ -9,7 +9,39 @@
from collections import defaultdict from collections import defaultdict
class Table: class Element:
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()
@ -22,33 +54,33 @@ class Table:
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,
data: dict[str, str] = None, attrs: 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
@ -59,24 +91,39 @@ class Table:
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 _prepare(self): def is_empty(self) -> bool:
"""Sort table elements before generation""" "true if table has no rows"
self.sort_columns() return len(self.rows) == 0
def html(self, classes: list[str] = None, data: dict[str, str] = None) -> str: def select_row(self, row_id):
"""HTML version of the table "mark rows as 'selected'"
classes are prepended to existing classes self.selected_row_id = row_id
data may replace existing data
""" def to_list(self) -> list[dict]:
"""as a list, each row is a dict"""
self._prepare() self._prepare()
classes = classes + self.classes return [row.to_dict() for row in self.rows]
data = self.data.copy().update(data)
elt_class = f"""class="{' '.join(classes)}" """ if classes else "" def html(self, extra_classes: list[str] = None) -> str:
attrs_str = " ".join(f' data-{k}="v"' for k, v in data.items()) """HTML version of the table"""
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"""
@ -96,13 +143,14 @@ class Table:
if self.foot if self.foot
else "" else ""
) )
return f"""<table {elt_class} {attrs_str}> return f"""
{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":
@ -131,8 +179,12 @@ class Table:
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: (groups_order.get(self.column_group.get(col_id), col_id)) key=lambda 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):
@ -165,23 +217,26 @@ class Table:
"""Set columns titles""" """Set columns titles"""
self.titles.update(titles) self.titles.update(titles)
def add_title(self, col_id, title: str = None) -> tuple["Cell", "Cell"]: def add_title(
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 not col_id in self.titles: if col_id not in self.titles:
self.titles[col_id] = title self.titles[col_id] = title
cell_head = self.head_title_row.cells.get( self.head_title_row.cells[col_id] = self.head_title_row.add_cell(
col_id, self.head_title_row.add_cell(col_id, title, title) col_id, None, title, classes=classes
) )
cell_foot = self.foot_title_row.cells.get( self.foot_title_row.cells[col_id] = self.foot_title_row.add_cell(
col_id, self.foot_title_row.add_cell(col_id, title, title) col_id, None, title, classes=classes
) )
return cell_head, cell_foot
return self.head_title_row.cells.get(col_id), self.foot_title_row.cells[col_id]
class Row: class Row(Element):
"""A row.""" """A row."""
def __init__( def __init__(
@ -191,13 +246,15 @@ class Row:
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
@ -211,23 +268,17 @@ class Row:
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 + [group or ""], # ajoute le nom de groupe aux classes (classes or []) + [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,
@ -243,28 +294,42 @@ class Row:
"""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) self.table.add_title(col_id, title, classes=cell.classes)
return cell return cell
def html(self) -> str: def html(self, extra_classes: list[str] = None) -> str:
"""html for row, with cells""" """html for row, with cells"""
elt_class = f"""class="{' '.join(self.classes)}" """ if self.classes else "" if (self.id is not None) and self.id == getattr(self.table, "selected_row_id"):
tr_id = ( self.classes.append("row_selected")
"""id="row_selected" """ if (self.id == self.table.selected_etudid) else "" return super().html(extra_classes=extra_classes)
) # TODO XXX remplacer id par une classe
return f"""<tr {tr_id} {elt_class}>{ def html_content(self) -> str:
"".join([self.cells.get(col_id, "Le contenu du row en html."
self.table.empty_cell).html( return "".join(
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):
@ -289,7 +354,7 @@ class BottomRow(Row):
self.add_cell(col_id, None, title) self.add_cell(col_id, None, title)
class Cell: class Cell(Element):
"""Une cellule de table""" """Une cellule de table"""
def __init__( def __init__(
@ -297,21 +362,21 @@ class Cell:
content, content,
classes: list[str] = None, classes: list[str] = None,
elt="td", elt="td",
attrs: list[str] = None, attrs: dict[str, 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"""
self.content = content super().__init__(
self.classes: list = classes or [] elt if elt is not None else "td", content, classes, attrs, data
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 # not yet used self.raw_content = raw_content or content
self.target = target self.target = target
self.target_attrs = target_attrs or {} self.target_attrs = target_attrs or {}
@ -323,20 +388,14 @@ class Cell:
def __str__(self): def __str__(self):
return str(self.content) return str(self.content)
def html(self, column_classes: list[str] = None) -> str: def html_content(self) -> str:
"html for cell" "content of the table cell, as html"
attrs_str = f"""class="{' '.join( # entoure le contenu par un lien ?
[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()]
) )
content = f"<a {href} {target_attrs_str}>{content}</a>" return f"<a {href} {target_attrs_str}>{super().html_content()}</a>"
return f"""<{self.elt} {attrs_str}>{self.content}</{self.elt}>"""
return super().html_content()

View File

@ -4029,6 +4029,10 @@ table.table_recap .rang {
text-align: right; text-align: right;
} }
table.table_recap .cursus {
white-space: nowrap;
}
table.table_recap .col_ue, table.table_recap .col_ue,
table.table_recap .col_ue_code, table.table_recap .col_ue_code,
table.table_recap .col_moy_gen, table.table_recap .col_moy_gen,

View File

@ -1,7 +1,7 @@
// Tableau recap notes // Tableau recap notes
$(function () { $(function () {
$(function () { $(function () {
let hidden_colums = ["codes", "identite_detail", "partition_aux", "partition_rangs", "admission", "col_empty"]; let hidden_colums = ["etud_codes", "identite_detail", "partition_aux", "partition_rangs", "admission", "col_empty"];
let mode_jury_but_bilan = $('table.table_recap').hasClass("table_jury_but_bilan"); let mode_jury_but_bilan = $('table.table_recap').hasClass("table_jury_but_bilan");
if (mode_jury_but_bilan) { if (mode_jury_but_bilan) {
// table bilan décisions: cache les notes // table bilan décisions: cache les notes
@ -247,7 +247,7 @@ $(function () {
}); });
// Pour montrer et surligner l'étudiant sélectionné: // Pour montrer et surligner l'étudiant sélectionné:
$(function () { $(function () {
let row_selected = document.querySelector("#row_selected"); let row_selected = document.querySelector(".row_selected");
if (row_selected) { if (row_selected) {
/*row_selected.scrollIntoView(); /*row_selected.scrollIntoView();
window.scrollBy(0, -50);*/ window.scrollBy(0, -50);*/