WIP: table jury.

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

View File

@ -4,7 +4,7 @@
# See LICENSE # 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
jury_stats est un dict donnant des comptages sur le jury. 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),

View File

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

View File

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

View File

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

View File

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

View File

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