forked from ScoDoc/ScoDoc
WIP: table jury.
This commit is contained in:
parent
d4da019c2e
commit
724d01c36a
@ -4,7 +4,7 @@
|
|||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
"""Jury BUT: table recap annuelle et liens saisie
|
"""Jury BUT et classiques: table recap annuelle et liens saisie
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
@ -19,15 +19,15 @@ from app.but.jury_but import (
|
|||||||
DecisionsProposeesUE,
|
DecisionsProposeesUE,
|
||||||
)
|
)
|
||||||
from app.comp.res_but import ResultatsSemestreBUT
|
from app.comp.res_but import ResultatsSemestreBUT
|
||||||
|
from app.comp.res_compat import NotesTableCompat
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
|
from app.models import UniteEns
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences
|
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences
|
||||||
from app.models.formsemestre import FormSemestre
|
from app.models.formsemestre import FormSemestre
|
||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
from app.scodoc.sco_codes_parcours import (
|
from app.scodoc.sco_codes_parcours import (
|
||||||
BUT_BARRE_RCUE,
|
BUT_BARRE_RCUE,
|
||||||
BUT_BARRE_UE,
|
|
||||||
BUT_BARRE_UE8,
|
|
||||||
BUT_RCUE_SUFFISANT,
|
BUT_RCUE_SUFFISANT,
|
||||||
)
|
)
|
||||||
from app.scodoc import sco_formsemestre_status
|
from app.scodoc import sco_formsemestre_status
|
||||||
@ -39,16 +39,97 @@ from app.tables.recap import RowRecap, TableRecap
|
|||||||
class TableJury(TableRecap):
|
class TableJury(TableRecap):
|
||||||
"""Cette table recap reprend les colonnes du tableau recap, sauf les évaluations,
|
"""Cette table recap reprend les colonnes du tableau recap, sauf les évaluations,
|
||||||
et ajoute:
|
et ajoute:
|
||||||
- les RCUEs
|
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
|
- 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):
|
class RowJury(RowRecap):
|
||||||
"Ligne de la table saisie jury"
|
"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"
|
"cell avec nb niveaux validables / total"
|
||||||
|
deca = self.deca
|
||||||
classes = ["col_rcue", "col_rcues_validables"]
|
classes = ["col_rcue", "col_rcues_validables"]
|
||||||
if deca.nb_rcues_under_8 > 0:
|
if deca.nb_rcues_under_8 > 0:
|
||||||
classes.append("moy_ue_warning")
|
classes.append("moy_ue_warning")
|
||||||
@ -78,42 +159,71 @@ class RowJury(RowRecap):
|
|||||||
"RCUEs",
|
"RCUEs",
|
||||||
f"""{deca.nb_validables}/{deca.nb_competences}"""
|
f"""{deca.nb_validables}/{deca.nb_competences}"""
|
||||||
+ ((" " + scu.EMO_WARNING) if deca.nb_rcues_under_8 > 0 else ""),
|
+ ((" " + 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,
|
classes=classes,
|
||||||
data={"order": order},
|
data={"order": order},
|
||||||
)
|
)
|
||||||
|
|
||||||
def add_ue_cells(self, dec_ue: DecisionsProposeesUE):
|
def add_ue_cols(self, ue: UniteEns, ue_status: dict):
|
||||||
"cell de moyenne d'UE"
|
"Ajoute 2 colonnes: moyenne d'UE et code jury"
|
||||||
col_id = f"moy_ue_{dec_ue.ue.id}"
|
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 = ""
|
note_class = ""
|
||||||
val = dec_ue.moy_ue
|
val = rcue.moy_rcue
|
||||||
if isinstance(val, float):
|
if isinstance(val, float):
|
||||||
if val < BUT_BARRE_UE:
|
if val < BUT_BARRE_RCUE:
|
||||||
note_class = "moy_inf"
|
note_class = "moy_ue_inf"
|
||||||
elif val >= BUT_BARRE_UE:
|
elif val >= BUT_BARRE_RCUE:
|
||||||
note_class = "moy_ue_valid"
|
note_class = "moy_ue_valid"
|
||||||
if val < BUT_BARRE_UE8:
|
if val < BUT_RCUE_SUFFISANT:
|
||||||
note_class = "moy_ue_warning" # notes très basses
|
note_class = "moy_ue_warning" # notes très basses
|
||||||
self.add_cell(
|
self.add_cell(
|
||||||
col_id,
|
col_id,
|
||||||
dec_ue.ue.acronyme,
|
f"<div>{rcue.ue_1.acronyme}</div><div>{rcue.ue_2.acronyme}</div>",
|
||||||
self.fmt_note(val),
|
self.table.fmt_note(val),
|
||||||
group="col_ue",
|
raw_content=val,
|
||||||
classes="col_ue" + note_class,
|
group="rcue",
|
||||||
column_class="col_ue",
|
classes=[note_class],
|
||||||
|
column_classes={"col_rcue"},
|
||||||
)
|
)
|
||||||
self.add_cell(
|
self.add_cell(
|
||||||
col_id + "_code",
|
col_id + "_code",
|
||||||
dec_ue.ue.acronyme,
|
f"<div>{rcue.ue_1.acronyme}</div><div>{rcue.ue_2.acronyme}</div>",
|
||||||
dec_ue.code_valide or "",
|
dec_rcue.code_valide or "",
|
||||||
classes="col_ue_code recorded_code",
|
group="rcue",
|
||||||
column_class="col_ue",
|
classes=["col_rcue_code", "recorded_code"],
|
||||||
|
column_classes={"col_rcue"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def formsemestre_saisie_jury_but(
|
def formsemestre_saisie_jury_but(
|
||||||
formsemestre2: FormSemestre,
|
formsemestre: FormSemestre,
|
||||||
read_only: bool = False,
|
read_only: bool = False,
|
||||||
selected_etudid: int = None,
|
selected_etudid: int = None,
|
||||||
mode="jury",
|
mode="jury",
|
||||||
@ -125,7 +235,6 @@ def formsemestre_saisie_jury_but(
|
|||||||
|
|
||||||
Si mode == "recap", table recap des codes, sans liens de saisie.
|
Si mode == "recap", table recap des codes, sans liens de saisie.
|
||||||
"""
|
"""
|
||||||
# Quick & Dirty
|
|
||||||
# pour chaque etud de res2 trié
|
# pour chaque etud de res2 trié
|
||||||
# S1: UE1, ..., UEn
|
# S1: UE1, ..., UEn
|
||||||
# S2: 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
|
# 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
|
# -> rcue .ue_1, .ue_2 -> stroe moy ues, rcue.moy_rcue, etc
|
||||||
|
|
||||||
if formsemestre2.formation.referentiel_competence is None:
|
if formsemestre.formation.referentiel_competence is None:
|
||||||
raise ScoNoReferentielCompetences(formation=formsemestre2.formation)
|
raise ScoNoReferentielCompetences(formation=formsemestre.formation)
|
||||||
|
|
||||||
rows, titles, column_ids, jury_stats = get_jury_but_table(
|
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
|
||||||
formsemestre2, read_only=read_only, mode=mode
|
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 (
|
return (
|
||||||
'<div class="table_recap"><div class="message">aucun étudiant !</div></div>'
|
'<div class="table_recap"><div class="message">aucun étudiant !</div></div>'
|
||||||
)
|
)
|
||||||
filename = scu.sanitize_filename(
|
table.data["filename"] = scu.sanitize_filename(
|
||||||
f"""jury-but-{formsemestre2.titre_num()}-{time.strftime("%Y-%m-%d")}"""
|
f"""jury-but-{formsemestre.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_html = table.html()
|
||||||
H = [
|
H = [
|
||||||
html_sco_header.sco_header(
|
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,
|
no_side_bar=True,
|
||||||
init_qtip=True,
|
init_qtip=True,
|
||||||
javascripts=["js/etud_info.js", "js/table_recap.js"],
|
javascripts=["js/etud_info.js", "js/table_recap.js"],
|
||||||
),
|
),
|
||||||
sco_formsemestre_status.formsemestre_status_head(
|
sco_formsemestre_status.formsemestre_status_head(
|
||||||
formsemestre_id=formsemestre2.id
|
formsemestre_id=formsemestre.id
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
if mode == "recap":
|
if mode == "recap":
|
||||||
@ -173,12 +290,12 @@ def formsemestre_saisie_jury_but(
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href="{url_for(
|
<li><a href="{url_for(
|
||||||
"notes.pvjury_table_but",
|
"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>
|
}" class="stdlink">Tableau PV de jury</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="{url_for(
|
<li><a href="{url_for(
|
||||||
"notes.formsemestre_lettres_individuelles",
|
"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>
|
}" class="stdlink">Courriers individuels (classeur pdf)</a>
|
||||||
</li>
|
</li>
|
||||||
</div>
|
</div>
|
||||||
@ -187,8 +304,9 @@ def formsemestre_saisie_jury_but(
|
|||||||
)
|
)
|
||||||
H.append(
|
H.append(
|
||||||
f"""
|
f"""
|
||||||
|
<div class="table_recap">
|
||||||
{table_html}
|
{table_html}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="table_jury_but_links">
|
<div class="table_jury_but_links">
|
||||||
"""
|
"""
|
||||||
@ -199,7 +317,7 @@ def formsemestre_saisie_jury_but(
|
|||||||
f"""
|
f"""
|
||||||
<p><a class="stdlink" href="{url_for(
|
<p><a class="stdlink" href="{url_for(
|
||||||
"notes.formsemestre_saisie_jury",
|
"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>
|
}">Saisie des décisions du jury</a>
|
||||||
</p>"""
|
</p>"""
|
||||||
)
|
)
|
||||||
@ -208,12 +326,12 @@ def formsemestre_saisie_jury_but(
|
|||||||
f"""
|
f"""
|
||||||
<p><a class="stdlink" href="{url_for(
|
<p><a class="stdlink" href="{url_for(
|
||||||
"notes.formsemestre_validation_auto_but",
|
"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>
|
}">Calcul automatique des décisions du jury</a>
|
||||||
</p>
|
</p>
|
||||||
<p><a class="stdlink" href="{url_for(
|
<p><a class="stdlink" href="{url_for(
|
||||||
"notes.formsemestre_jury_but_recap",
|
"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>
|
}">Tableau récapitulatif des décisions du jury</a>
|
||||||
</p>
|
</p>
|
||||||
"""
|
"""
|
||||||
@ -224,19 +342,19 @@ def formsemestre_saisie_jury_but(
|
|||||||
|
|
||||||
<div class="jury_stats">
|
<div class="jury_stats">
|
||||||
<div>Nb d'étudiants avec décision annuelle:
|
<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>
|
||||||
<div><b>Codes annuels octroyés:</b></div>
|
<div><b>Codes annuels octroyés:</b></div>
|
||||||
<table class="jury_stats_codes">
|
<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(
|
H.append(
|
||||||
f"""<tr>
|
f"""<tr>
|
||||||
<td>{code}</td>
|
<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">{
|
<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>
|
</td>
|
||||||
</tr>"""
|
</tr>"""
|
||||||
)
|
)
|
||||||
@ -346,43 +464,16 @@ class RowCollector:
|
|||||||
self.column_classes[col_id] = column_class
|
self.column_classes[col_id] = column_class
|
||||||
self.idx += 1
|
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( # XXX A SUPPRIMER apres avoir recupéré les stats
|
||||||
def get_jury_but_table(
|
|
||||||
formsemestre2: FormSemestre, read_only: bool = False, mode="jury", with_links=True
|
formsemestre2: FormSemestre, read_only: bool = False, mode="jury", with_links=True
|
||||||
) -> tuple[list[dict], list[str], list[str], dict]:
|
) -> tuple[list[dict], list[str], list[str], dict]:
|
||||||
"""Construit la table des résultats annuels pour le jury BUT
|
"""Construit la table des résultats annuels pour le jury BUT
|
||||||
=> rows_dict, titles, column_ids, jury_stats
|
=> rows_dict, titles, column_ids, jury_stats
|
||||||
où jury_stats est un dict donnant des comptages sur le jury.
|
où jury_stats est un dict donnant des comptages sur le jury.
|
||||||
"""
|
"""
|
||||||
res2: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre2)
|
|
||||||
|
# /////// XXX /////// XXX //////
|
||||||
titles = {} # column_id : title
|
titles = {} # column_id : title
|
||||||
jury_stats = {
|
jury_stats = {
|
||||||
"nb_etuds": len(formsemestre2.etuds_inscriptions),
|
"nb_etuds": len(formsemestre2.etuds_inscriptions),
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from app import db
|
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.comp.res_cache import ResultatsCache
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
from app.scodoc import sco_codes_parcours
|
from app.scodoc import sco_codes_parcours
|
||||||
@ -53,7 +53,7 @@ class ValidationsSemestre(ResultatsCache):
|
|||||||
self.comp_decisions_jury()
|
self.comp_decisions_jury()
|
||||||
|
|
||||||
def comp_decisions_jury(self):
|
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:
|
Calcule les attributs:
|
||||||
decisions_jury = { etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }}
|
decisions_jury = { etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }}
|
||||||
decision_jury_ues={ etudid :
|
decision_jury_ues={ etudid :
|
||||||
@ -102,6 +102,12 @@ class ValidationsSemestre(ResultatsCache):
|
|||||||
|
|
||||||
self.decisions_jury_ues = decisions_jury_ues
|
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:
|
def formsemestre_get_ue_capitalisees(formsemestre: FormSemestre) -> pd.DataFrame:
|
||||||
"""Liste des UE capitalisées (ADM) utilisables dans ce formsemestre
|
"""Liste des UE capitalisées (ADM) utilisables dans ce formsemestre
|
||||||
|
@ -4020,6 +4020,11 @@ table.table_recap tbody td:hover {
|
|||||||
text-decoration: dashed underline;
|
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 */
|
/* col moy gen en gras seulement pour les form. classiques */
|
||||||
table.table_recap.classic td.col_moy_gen {
|
table.table_recap.classic td.col_moy_gen {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@ -4039,10 +4044,10 @@ table.table_recap .cursus {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.table_recap .col_ue,
|
table.table_recap td.col_ue,
|
||||||
table.table_recap .col_ue_code,
|
table.table_recap td.col_ue_code,
|
||||||
table.table_recap .col_moy_gen,
|
table.table_recap td.col_moy_gen,
|
||||||
table.table_recap .group {
|
table.table_recap td.group {
|
||||||
border-left: 1px solid blue;
|
border-left: 1px solid blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4050,7 +4055,7 @@ table.table_recap .col_ue {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.table_recap.jury .col_ue {
|
table.table_recap.jury td.col_ue {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4059,29 +4064,53 @@ table.table_recap.jury .col_rcue_code {
|
|||||||
font-weight: bold;
|
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,
|
||||||
table.table_recap.jury tr.odd td.col_rcue_code {
|
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 {
|
/* table.table_recap.jury tr.even td.col_rcue,
|
||||||
background-color: #e1d3c5 !important;
|
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 {
|
table.table_recap.jury tr.even td.col_rcues_validables {
|
||||||
background-color: #fcebda !important;
|
background-color: #e5f2ff !important;
|
||||||
}
|
} */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
table.table_recap .group {
|
table.table_recap .group {
|
||||||
border-left: 1px dashed rgb(160, 160, 160);
|
border-left: 1px dashed rgb(160, 160, 160);
|
||||||
white-space: nowrap;
|
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 {
|
table.table_recap .admission {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
color: rgb(6, 73, 6);
|
color: rgb(6, 73, 6);
|
||||||
@ -4250,7 +4279,7 @@ table.table_recap tr.apo td {
|
|||||||
max-width: 1px;
|
max-width: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.table_recap tr.type_col {
|
table.table_recap tr.type_col td {
|
||||||
font-size: 40%;
|
font-size: 40%;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
|
@ -10,8 +10,6 @@ $(function () {
|
|||||||
if (mode_jury_but_bilan) {
|
if (mode_jury_but_bilan) {
|
||||||
// table bilan décisions: cache les notes
|
// table bilan décisions: cache les notes
|
||||||
hidden_colums = hidden_colums.concat(["col_ue", "col_rcue", "col_lien_saisie_but"]);
|
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:
|
// Etat (tri des colonnes) de la table:
|
||||||
|
|
||||||
|
@ -54,8 +54,12 @@ class TableRecap(tb.Table):
|
|||||||
convert_values=False,
|
convert_values=False,
|
||||||
include_evaluations=False,
|
include_evaluations=False,
|
||||||
mode_jury=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.res = res
|
||||||
self.include_evaluations = include_evaluations
|
self.include_evaluations = include_evaluations
|
||||||
self.mode_jury = mode_jury
|
self.mode_jury = mode_jury
|
||||||
@ -72,13 +76,12 @@ class TableRecap(tb.Table):
|
|||||||
# couples (modimpl, ue) effectivement présents dans la table:
|
# couples (modimpl, ue) effectivement présents dans la table:
|
||||||
self.modimpl_ue_ids = set()
|
self.modimpl_ue_ids = set()
|
||||||
|
|
||||||
etuds_inscriptions = res.formsemestre.etuds_inscriptions
|
|
||||||
ues = res.formsemestre.query_ues(with_sport=True) # avec bonus
|
ues = res.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]
|
||||||
|
|
||||||
for etudid in etuds_inscriptions:
|
for etudid in res.formsemestre.etuds_inscriptions:
|
||||||
etud = Identite.query.get(etudid)
|
etud = Identite.query.get(etudid)
|
||||||
row = RowRecap(self, etud)
|
row = self.row_class(self, etud)
|
||||||
self.add_row(row)
|
self.add_row(row)
|
||||||
row.add_etud_cols()
|
row.add_etud_cols()
|
||||||
row.add_moyennes_cols(ues_sans_bonus)
|
row.add_moyennes_cols(ues_sans_bonus)
|
||||||
@ -100,11 +103,14 @@ class TableRecap(tb.Table):
|
|||||||
if include_evaluations:
|
if include_evaluations:
|
||||||
self.add_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.mark_empty_cols()
|
||||||
self.add_type_row()
|
self.add_type_row()
|
||||||
|
|
||||||
def mark_empty_cols(self):
|
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
|
# identifie les col. vides par la classe sur leur moyenne
|
||||||
row_moy = self.get_row_by_id("moy")
|
row_moy = self.get_row_by_id("moy")
|
||||||
for col_id in self.column_ids:
|
for col_id in self.column_ids:
|
||||||
@ -275,11 +281,12 @@ class TableRecap(tb.Table):
|
|||||||
|
|
||||||
def add_partitions(self):
|
def add_partitions(self):
|
||||||
"""Ajoute les colonnes indiquant les groupes
|
"""Ajoute les colonnes indiquant les groupes
|
||||||
|
pour tous les étudiants de la table.
|
||||||
La table contient des rows avec la clé etudid.
|
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.insert_group("partition", after="identite_court")
|
||||||
|
self.group_titles["partition"] = "Partitions"
|
||||||
partitions, partitions_etud_groups = sco_groups.get_formsemestre_groups(
|
partitions, partitions_etud_groups = sco_groups.get_formsemestre_groups(
|
||||||
self.res.formsemestre.id
|
self.res.formsemestre.id
|
||||||
)
|
)
|
||||||
@ -404,6 +411,7 @@ class TableRecap(tb.Table):
|
|||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
evaluation_id=e.id,
|
evaluation_id=e.id,
|
||||||
),
|
),
|
||||||
|
target_attrs={"class": "stdlink"},
|
||||||
)
|
)
|
||||||
|
|
||||||
def add_admissions(self):
|
def add_admissions(self):
|
||||||
@ -478,6 +486,13 @@ class RowRecap(tb.Row):
|
|||||||
"""Ajoute colonnes étudiant: codes, noms"""
|
"""Ajoute colonnes étudiant: codes, noms"""
|
||||||
res = self.table.res
|
res = self.table.res
|
||||||
etud = self.etud
|
etud = self.etud
|
||||||
|
self.table.group_titles.update(
|
||||||
|
{
|
||||||
|
"etud_codes": "Codes",
|
||||||
|
"identite_detail": "",
|
||||||
|
"identite_court": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
# --- 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")
|
self.add_cell("etudid", "etudid", etud.id, "etud_codes")
|
||||||
self.add_cell(
|
self.add_cell(
|
||||||
@ -614,35 +629,11 @@ class RowRecap(tb.Row):
|
|||||||
data={"order": self.nb_ues_validables}, # tri
|
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):
|
def add_ue_cols(self, ue: UniteEns, ue_status: dict):
|
||||||
"Ajoute résultat UE au row (colonne col_ue)"
|
"Ajoute résultat UE au row (colonne col_ue)"
|
||||||
|
# sous-classé par JuryRow pour ajouter les codes
|
||||||
table = self.table
|
table = self.table
|
||||||
|
table.group_titles["col_ue"] = "UEs du semestre"
|
||||||
col_id = f"moy_ue_{ue.id}"
|
col_id = f"moy_ue_{ue.id}"
|
||||||
val = ue_status["moy"]
|
val = ue_status["moy"]
|
||||||
note_class = ""
|
note_class = ""
|
||||||
@ -659,9 +650,9 @@ class RowRecap(tb.Row):
|
|||||||
col_id,
|
col_id,
|
||||||
ue.acronyme,
|
ue.acronyme,
|
||||||
table.fmt_note(val),
|
table.fmt_note(val),
|
||||||
group=f"col_ue_{ue.id}",
|
group="col_ue",
|
||||||
classes=[note_class],
|
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[
|
table.foot_title_row.cells[col_id].target_attrs[
|
||||||
"title"
|
"title"
|
||||||
|
@ -73,8 +73,10 @@ class Table(Element):
|
|||||||
classes: list[str] = None,
|
classes: list[str] = None,
|
||||||
attrs: dict[str, str] = None,
|
attrs: dict[str, str] = None,
|
||||||
data: dict = None,
|
data: dict = None,
|
||||||
|
row_class=None,
|
||||||
):
|
):
|
||||||
super().__init__("table", classes=classes, attrs=attrs, data=data)
|
super().__init__("table", classes=classes, attrs=attrs, data=data)
|
||||||
|
self.row_class = row_class or Row
|
||||||
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"] = {}
|
||||||
@ -82,6 +84,8 @@ class Table(Element):
|
|||||||
"ordered list of columns ids"
|
"ordered list of columns ids"
|
||||||
self.groups = []
|
self.groups = []
|
||||||
"ordered list of column groups names"
|
"ordered list of column groups names"
|
||||||
|
self.group_titles = {}
|
||||||
|
"title (in header top row) for the group"
|
||||||
self.head = []
|
self.head = []
|
||||||
self.foot = []
|
self.foot = []
|
||||||
self.column_group = {}
|
self.column_group = {}
|
||||||
@ -92,8 +96,12 @@ class Table(Element):
|
|||||||
"l'id de la ligne sélectionnée"
|
"l'id de la ligne sélectionnée"
|
||||||
self.titles = {}
|
self.titles = {}
|
||||||
"Column title: { col_id : titre }"
|
"Column title: { col_id : titre }"
|
||||||
self.head_title_row: "Row" = Row(self, "title_head", cell_elt="th")
|
self.head_title_row: "Row" = Row(
|
||||||
self.foot_title_row: "Row" = Row(self, "title_foot", cell_elt="th")
|
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()
|
self.empty_cell = Cell.empty()
|
||||||
|
|
||||||
def _prepare(self):
|
def _prepare(self):
|
||||||
@ -109,6 +117,10 @@ class Table(Element):
|
|||||||
"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 __len__(self):
|
||||||
|
"nombre de lignes dans le corps de la table"
|
||||||
|
return len(self.rows)
|
||||||
|
|
||||||
def is_empty(self) -> bool:
|
def is_empty(self) -> bool:
|
||||||
"true if table has no rows"
|
"true if table has no rows"
|
||||||
return len(self.rows) == 0
|
return len(self.rows) == 0
|
||||||
@ -166,7 +178,6 @@ class Table(Element):
|
|||||||
|
|
||||||
def add_head_row(self, row: "Row") -> "Row":
|
def add_head_row(self, row: "Row") -> "Row":
|
||||||
"Add a row to table head"
|
"Add a row to table head"
|
||||||
# row = Row(self, cell_elt="th", category="head")
|
|
||||||
self.head.append(row)
|
self.head.append(row)
|
||||||
self.row_by_id[row.id] = row
|
self.row_by_id[row.id] = row
|
||||||
return row
|
return row
|
||||||
@ -177,6 +188,16 @@ class Table(Element):
|
|||||||
self.row_by_id[row.id] = row
|
self.row_by_id[row.id] = row
|
||||||
return 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):
|
def sort_rows(self, key: callable, reverse: bool = False):
|
||||||
"""Sort table rows"""
|
"""Sort table rows"""
|
||||||
self.rows.sort(key=key, reverse=reverse)
|
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 f"<a {href} {target_attrs_str}>{super().html_content()}</a>"
|
||||||
|
|
||||||
return super().html_content()
|
return super().html_content()
|
||||||
|
|
||||||
|
|
||||||
|
class RowGroupsHeader(Row):
|
||||||
|
"""Header line at the top of the table
|
||||||
|
with a multicolumn th cell per group
|
||||||
|
"""
|
||||||
|
|
||||||
|
def html_content(self):
|
||||||
|
"""Le contenu est généré intégralement ici: un th par groupe contigu
|
||||||
|
Note: les colonnes doivent avoir déjà été triées par table.sort_columns()
|
||||||
|
"""
|
||||||
|
column_ids = self.table.column_ids
|
||||||
|
nb_cols = len(self.table.column_ids)
|
||||||
|
elements = []
|
||||||
|
idx = 0
|
||||||
|
while idx < nb_cols:
|
||||||
|
group_title = self.table.group_titles.get(
|
||||||
|
self.table.column_group.get(column_ids[idx])
|
||||||
|
)
|
||||||
|
colspan = 1
|
||||||
|
idx += 1
|
||||||
|
# on groupe tant que les TITRES des groupes sont identiques
|
||||||
|
while idx < nb_cols and group_title == self.table.group_titles.get(
|
||||||
|
self.table.column_group.get(column_ids[idx])
|
||||||
|
):
|
||||||
|
idx += 1
|
||||||
|
colspan += 1
|
||||||
|
elements.append(f"""<th colspan="{colspan}">{group_title or ""}</th>""")
|
||||||
|
|
||||||
|
return "\n".join(elements) if len(elements) > 1 else ""
|
||||||
|
Loading…
Reference in New Issue
Block a user