WIP: nouveau tableau recap pour le BUT

This commit is contained in:
Emmanuel Viennet 2022-03-26 23:33:57 +01:00
parent b68d43454f
commit 13f55d5190
9 changed files with 437 additions and 21 deletions

View File

@ -15,10 +15,12 @@ from app.comp import moy_ue, moy_sem, inscr_mod
from app.comp.res_common import NotesTableCompat
from app.comp.bonus_spo import BonusSport
from app.models import ScoDocSiteConfig
from app.models.etudiants import Identite
from app.models.moduleimpls import ModuleImpl
from app.models.ues import UniteEns
from app.scodoc import sco_preferences
from app.scodoc.sco_codes_parcours import UE_SPORT
import app.scodoc.sco_utils as scu
class ResultatsSemestreBUT(NotesTableCompat):
@ -32,6 +34,10 @@ class ResultatsSemestreBUT(NotesTableCompat):
def __init__(self, formsemestre):
super().__init__(formsemestre)
self.modimpl_coefs_df = None
"""DataFrame, row UEs(sans bonus), cols modimplid, value coef"""
self.sem_cube = None
"""ndarray (etuds x modimpl x ue)"""
if not self.load_cached():
t0 = time.time()
@ -145,15 +151,228 @@ class ResultatsSemestreBUT(NotesTableCompat):
"""
return self.modimpl_coefs_df.loc[ue.id].sum()
def modimpls_in_ue(self, ue_id, etudid) -> list[ModuleImpl]:
def modimpls_in_ue(self, ue_id, etudid, with_bonus=True) -> list[ModuleImpl]:
"""Liste des modimpl ayant des coefs non nuls vers cette UE
et auxquels l'étudiant est inscrit.
et auxquels l'étudiant est inscrit. Inclus modules bonus le cas échéant.
"""
# sert pour l'affichage ou non de l'UE sur le bulletin
# sert pour l'affichage ou non de l'UE sur le bulletin et la table recap
coefs = self.modimpl_coefs_df # row UE, cols modimpl
return [
modimpls = [
modimpl
for modimpl in self.formsemestre.modimpls_sorted
if (coefs[modimpl.id][ue_id] != 0)
and self.modimpl_inscr_df[modimpl.id][etudid]
]
if not with_bonus:
return [
modimpl for modimpl in modimpls if modimpl.module.ue.type != UE_SPORT
]
return modimpls
def get_table_moyennes_triees(self, convert_values=False) -> list:
"""Result: tuple avec
- rows: liste de dicts { column_id : value }
- titles: { column_id : title }
- columns_ids: (liste des id de colonnes)
. Si convert_values, transforme les notes en chaines ("12.34").
Les colonnes générées sont:
etudid
rang : rang indicatif (basé sur moy gen)
moy_gen : moy gen indicative
moy_ue_<ue_id>, ..., les moyennes d'UE
moy_res_<modimpl_id>_<ue_id>, ... les moyennes de ressources dans l'UE
moy_sae_<modimpl_id>_<ue_id>, ... les moyennes de SAE dans l'UE
On ajoute aussi des attributs:
- pour les lignes:
_css_row_class (inutilisé pour le monent)
_<column_id>_class classe css:
- la moyenne générale a la classe col_moy_gen
- les colonnes SAE ont la classe col_sae
- les colonnes Resources ont la classe col_res
- les colonnes d'UE ont la classe col_ue
- les colonnes de modules (SAE ou res.) d'une UE ont la classe mod_ue_<ue_id>
_<column_id>_order : clé de tri
"""
def fmt_note(x):
return scu.fmt_note(x) if convert_values else x
barre_moy = (
self.formsemestre.formation.get_parcours().BARRE_MOY - scu.NOTES_TOLERANCE
)
barre_valid_ue = self.formsemestre.formation.get_parcours().NOTES_BARRE_VALID_UE
NO_NOTE = "-" # contenu des cellules sans notes
rows = []
titles = {"rang": "Rg"} # column_id : title
def add_cell(
row: dict, col_id: str, title: str, content: str, classes: str = ""
):
"Add a row to our table. classes is a list of css class names"
row[col_id] = content
if classes:
row[f"_{col_id}_class"] = classes
if not col_id in titles:
titles[col_id] = title
if classes:
titles[f"_{col_id}_class"] = classes
etuds_inscriptions = self.formsemestre.etuds_inscriptions
ues = self.formsemestre.query_ues(with_sport=True) # avec bonus
modimpl_ids = set() # modimpl effectivement présents dans la table
for etudid in etuds_inscriptions:
etud = Identite.query.get(etudid)
row = {"etudid": etudid}
# --- Rang
add_cell(row, "rang", "Rg", self.etud_moy_gen_ranks[etudid], "rang")
row["_rang_order"] = f"{self.etud_moy_gen_ranks_int[etudid]:05d}"
# --- Identité étudiant
add_cell(row, "civilite_str", "Civ.", etud.civilite_str, "identite_detail")
add_cell(row, "nom_disp", "Nom", etud.nom_disp(), "identite_detail")
add_cell(row, "prenom", "Prénom", etud.prenom, "identite_detail")
add_cell(row, "nom_short", "Nom", etud.nom_short, "identite_court")
# --- Moyenne générale
moy_gen = self.etud_moy_gen.get(etudid, False)
note_class = ""
if moy_gen is False:
moy_gen = NO_NOTE
elif isinstance(moy_gen, float) and moy_gen < barre_moy:
note_class = " moy_inf"
add_cell(
row,
"moy_gen",
"Moy",
fmt_note(moy_gen),
"col_moy_gen" + note_class,
)
# --- Moyenne d'UE
for ue in [ue for ue in ues if ue.type != UE_SPORT]:
ue_status = self.get_etud_ue_status(etudid, ue.id)
if ue_status is not None:
col_id = f"moy_ue_{ue.id}"
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"
add_cell(
row,
col_id,
ue.acronyme,
fmt_note(val),
"col_ue" + note_class,
)
# Les moyennes des ressources et SAÉs dans cette UE
for modimpl in self.modimpls_in_ue(ue.id, etudid, with_bonus=False):
if ue_status["is_capitalized"]:
val = "-c-"
else:
modimpl_results = self.modimpls_results.get(modimpl.id)
if modimpl_results: # pas bonus
moys_vers_ue = modimpl_results.etuds_moy_module.get(
ue.id
)
val = (
moys_vers_ue.get(etudid, "?")
if moys_vers_ue is not None
else ""
)
else:
val = ""
col_id = (
f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}"
)
add_cell(
row,
col_id,
modimpl.module.code,
fmt_note(val),
# class col_res mod_ue_123
f"col_{modimpl.module.type_abbrv()} mod_ue_{ue.id}",
)
modimpl_ids.add(modimpl.id)
rows.append(row)
# tri par rang croissant
rows.sort(key=lambda e: e["_rang_order"])
# INFOS POUR FOOTER
bottom_infos = self._bottom_infos(
[ue for ue in ues if ue.type != UE_SPORT], modimpl_ids, fmt_note
)
# --- TABLE FOOTER: ECTS, moyennes, min, max...
footer_rows = []
for bottom_line in bottom_infos:
row = bottom_infos[bottom_line]
# Cases vides à styler:
row["moy_gen"] = row.get("moy_gen", "")
row["_moy_gen_class"] = "col_moy_gen"
# titre de la ligne:
row["nom_disp"] = row["nom_short"] = bottom_line.capitalize()
row["_tr_class"] = bottom_line.lower()
footer_rows.append(row)
return (
rows,
footer_rows,
titles,
[title for title in titles if not title.startswith("_")],
)
def _bottom_infos(self, ues, modimpl_ids: set, fmt_note) -> dict:
"""Les informations à mettre en bas de la table: min, max, moy, ECTS"""
bottom_infos = { # { key : row } avec key = min, max, moy, coef
"min": {},
"max": {},
"moy": {},
"coef": {},
}
# --- ECTS
row = {}
for ue in ues:
row[f"moy_ue_{ue.id}"] = ue.ects
row[f"_moy_ue_{ue.id}_class"] = "col_ue"
# style cases vides pour borders verticales
bottom_infos["coef"][f"moy_ue_{ue.id}"] = ""
bottom_infos["coef"][f"_moy_ue_{ue.id}_class"] = "col_ue"
row["moy_gen"] = sum([ue.ects or 0 for ue in ues if ue.type != UE_SPORT])
row["_moy_gen_class"] = "col_moy_gen"
bottom_infos["ects"] = row
# --- MIN, MAX, MOY
row_min, row_max, row_moy = {}, {}, {}
row_min["moy_gen"] = fmt_note(self.etud_moy_gen.min())
row_max["moy_gen"] = fmt_note(self.etud_moy_gen.max())
row_moy["moy_gen"] = fmt_note(self.etud_moy_gen.mean())
for ue in [ue for ue in ues if ue.type != UE_SPORT]:
col_id = f"moy_ue_{ue.id}"
row_min[col_id] = fmt_note(self.etud_moy_ue[ue.id].min())
row_max[col_id] = fmt_note(self.etud_moy_ue[ue.id].max())
row_moy[col_id] = fmt_note(self.etud_moy_ue[ue.id].mean())
row_min[f"_{col_id}_class"] = "col_ue"
row_max[f"_{col_id}_class"] = "col_ue"
row_moy[f"_{col_id}_class"] = "col_ue"
for modimpl in self.formsemestre.modimpls_sorted:
if modimpl.id in modimpl_ids:
col_id = f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}"
bottom_infos["coef"][col_id] = fmt_note(
self.modimpl_coefs_df[modimpl.id][ue.id]
)
i = self.modimpl_coefs_df.columns.get_loc(modimpl.id)
j = self.modimpl_coefs_df.index.get_loc(ue.id)
notes = self.sem_cube[:, i, j]
row_min[col_id] = fmt_note(np.nanmin(notes))
row_max[col_id] = fmt_note(np.nanmax(notes))
row_moy[col_id] = fmt_note(np.nanmean(notes))
bottom_infos["min"] = row_min
bottom_infos["max"] = row_max
bottom_infos["moy"] = row_moy
return bottom_infos

View File

@ -123,6 +123,11 @@ class Identite(db.Model):
r.append("-".join([x.lower().capitalize() for x in fields]))
return " ".join(r)
@property
def nom_short(self):
"Nom et début du prénom pour table recap: 'DUPONT Pi.'"
return f"{(self.nom_usuel or self.nom or '?').upper()} {(self.prenom or '')[:2].capitalize()}."
@cached_property
def sort_key(self) -> tuple:
"clé pour tris par ordre alphabétique"

View File

@ -33,7 +33,7 @@ class Module(db.Model):
numero = db.Column(db.Integer) # ordre de présentation
# id de l'element pedagogique Apogee correspondant:
code_apogee = db.Column(db.String(APO_CODE_STR_LEN))
# Type: ModuleType: DEFAULT, MALUS, RESSOURCE, MODULE_SAE (enum)
# Type: ModuleType.STANDARD, MALUS, RESSOURCE, SAE (enum)
module_type = db.Column(db.Integer, nullable=False, default=0, server_default="0")
# Relations:
modimpls = db.relationship("ModuleImpl", backref="module", lazy="dynamic")
@ -76,6 +76,11 @@ class Module(db.Model):
def type_name(self):
return scu.MODULE_TYPE_NAMES[self.module_type]
def type_abbrv(self):
""" "mod", "malus", "res", "sae"
(utilisées pour style css)"""
return scu.ModuleType.get_abbrev(self.module_type)
def set_ue_coef(self, ue, coef: float) -> None:
"""Set coef module vers cette UE"""
self.update_ue_coef_dict({ue.id: coef})

View File

@ -4,7 +4,6 @@
from app import db
from app.models import APO_CODE_STR_LEN
from app.models import SHORT_STR_LEN
from app.scodoc import notesdb as ndb
from app.scodoc import sco_utils as scu

View File

@ -38,6 +38,7 @@ from flask import make_response, url_for
from app import log
from app.but import bulletin_but
from app.comp import res_sem
from app.comp.res_but import ResultatsSemestreBUT
from app.comp.res_common import NotesTableCompat
from app.models import FormSemestre
from app.models.etudiants import Identite
@ -108,7 +109,7 @@ def formsemestre_recapcomplet(
page_title="Récapitulatif",
no_side_bar=True,
init_qtip=True,
javascripts=["js/etud_info.js"],
javascripts=["js/etud_info.js", "js/table_recap.js"],
),
sco_formsemestre_status.formsemestre_status_head(
formsemestre_id=formsemestre_id
@ -223,19 +224,28 @@ def do_formsemestre_recapcomplet(
force_publishing=True,
):
"""Calcule et renvoie le tableau récapitulatif."""
data, filename, format = make_formsemestre_recapcomplet(
formsemestre_id=formsemestre_id,
format=format,
hidemodules=hidemodules,
hidebac=hidebac,
xml_nodate=xml_nodate,
modejury=modejury,
sortcol=sortcol,
xml_with_decisions=xml_with_decisions,
disable_etudlink=disable_etudlink,
rank_partition_id=rank_partition_id,
force_publishing=force_publishing,
)
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if (
formsemestre.formation.is_apc()
and format not in ("xml", "json")
and not modejury
):
data, filename = make_formsemestre_recapcomplet_apc(formsemestre, format=format)
else:
data, filename, format = make_formsemestre_recapcomplet(
formsemestre_id=formsemestre_id,
format=format,
hidemodules=hidemodules,
hidebac=hidebac,
xml_nodate=xml_nodate,
modejury=modejury,
sortcol=sortcol,
xml_with_decisions=xml_with_decisions,
disable_etudlink=disable_etudlink,
rank_partition_id=rank_partition_id,
force_publishing=force_publishing,
)
# ---
if format == "xml" or format == "html":
return data
elif format == "csv":
@ -1004,3 +1014,59 @@ def formsemestres_bulletins(annee_scolaire):
jslist.append(J)
return scu.sendJSON(jslist)
def _gen_cell(key: str, row: dict, elt="td"):
"html table cell"
klass = row.get(f"_{key}_class")
attrs = f'class="{klass}"' if klass else ""
order = row.get("_{key}_order")
if order:
attrs += f' data-order="{order}"'
return f'<{elt} {attrs}>{row.get(key, "")}</{elt}>'
def _gen_row(keys: list[str], row, elt="td"):
klass = row.get("_tr_class")
tr_class = f'class="{klass}"' if klass else ""
return f'<tr {tr_class}>{"".join([_gen_cell(key, row, elt) for key in keys])}</tr>'
def make_formsemestre_recapcomplet_apc(formsemestre: FormSemestre, format="html"):
"""Construit table recap pour le BUT
Return: data, filename
"""
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
rows, footer_rows, titles, column_ids = res.get_table_moyennes_triees(
convert_values=True
)
H = ['<div class="table_recap"><table class="table_recap">']
# header
H.append(
f"""
<thead>
{_gen_row(column_ids, titles, "th")}
</thead>
"""
)
# body
H.append("<tbody>")
for row in rows:
H.append(f"{_gen_row(column_ids, row)}\n")
H.append("</tbody>\n")
# footer
H.append("<tfoot>")
for row in footer_rows:
H.append(f"{_gen_row(column_ids, row)}\n")
H.append(
f"""
{_gen_row(column_ids, titles, "th")}
</tfoot>
</table>
</div>
"""
)
return (
"".join(H),
f'recap-{formsemestre.titre_num().replace(" ", "_")}-{time.strftime("%d-%m-%Y")}',
) # suffix ?

View File

@ -87,6 +87,19 @@ class ModuleType(IntEnum):
RESSOURCE = 2 # BUT
SAE = 3 # BUT
@classmethod
def get_abbrev(cls, code) -> str:
"""Chaine abregée décrivant le type de module à partir du code integer:
"mod", "malus", "res", "sae"
(utilisées pour style css)
"""
return {
ModuleType.STANDARD: "mod",
ModuleType.MALUS: "malus",
ModuleType.RESSOURCE: "res",
ModuleType.SAE: "sae",
}.get(code, "???")
MODULE_TYPE_NAMES = {
ModuleType.STANDARD: "Module",

View File

@ -3238,3 +3238,45 @@ table.dataTable tr.gt_lastrow th {
table.dataTable td.etudinfo, table.dataTable td.group {
text-align: left;
}
/* Nouveau tableau recap */
div.table_recap table.table_recap {
width: auto;
}
table.table_recap .identite_court {
white-space:nowrap;
text-align: left;
}
table.table_recap .rang {
white-space:nowrap;
text-align: right;
}
table.table_recap .col_ue, table.table_recap .col_moy_gen {
border-left: 1px solid blue;
}
table.table_recap tfoot th, table.table_recap thead th {
text-align: left;
padding-left: 10px !important;
}
table.table_recap td.moy_inf {
font-weight: bold;
color: rgb(255,0,0);
}
table.table_recap td.moy_ue_valid {
font-weight: bold;
color: rgb(0,140,0);
}
table.table_recap tr.ects td {
color: rgb(160, 86, 3);
font-weight: bold;
border-bottom: 1px solid blue;
}
table.table_recap tr.coef td {
font-style: italic;
color: #9400d3;
}
table.table_recap tr.coef td, table.table_recap tr.min td,
table.table_recap tr.max td, table.table_recap tr.moy td {
font-size: 80%;
padding-top: 3px;
padding-bottom: 3px;
}

View File

@ -0,0 +1,67 @@
// Tableau recap notes
$(function () {
$(function () {
$('table.table_recap').DataTable(
{
paging: false,
searching: true,
info: false,
autoWidth: false,
fixedHeader: {
header: true,
footer: true
},
orderCellsTop: true, // cellules ligne 1 pour tri
aaSorting: [], // Prevent initial sorting
colReorder: true,
"columnDefs": [
{
// cache le détail de l'identité (pas réussi à le faire avec le sélecteur css)
"targets": [1, 2, 3], // ".identite_detail",
"visible": false,
},
],
dom: 'Bfrtip',
buttons: [
'copy', 'excel', 'pdf',
{
extend: 'collection',
text: 'Réglages affichage',
autoClose: true,
buttons: [
{
name: "toggle_ident",
text: "Civ/Nom/Prénom",
action: function (e, dt, node, config) {
let visible = dt.columns(".identite_detail").visible()[0];
dt.columns(".identite_detail").visible(!visible);
dt.columns(".identite_court").visible(visible);
dt.buttons('toggle_ident:name').text(visible ? "Civ/Nom/Prénom" : "Nom");
}
},
{
name: "toggle_res",
text: "Cacher les ressources",
action: function (e, dt, node, config) {
let visible = dt.columns(".col_res").visible()[0];
dt.columns(".col_res").visible(!visible);
dt.buttons('toggle_res:name').text(visible ? "Montrer les ressources" : "Cacher les ressources");
}
},
{
name: "toggle_sae",
text: "Cacher les SAÉs",
action: function (e, dt, node, config) {
let visible = dt.columns(".col_sae").visible()[0];
dt.columns(".col_sae").visible(!visible);
dt.buttons('toggle_sae:name').text(visible ? "Montrer les SAÉs" : "Cacher les SAÉs");
}
},
]
}
]
}
);
});
});

View File

@ -1,7 +1,7 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
SCOVERSION = "9.1.86"
SCOVERSION = "9.2-86"
SCONAME = "ScoDoc"