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.res_common import NotesTableCompat
from app.comp.bonus_spo import BonusSport from app.comp.bonus_spo import BonusSport
from app.models import ScoDocSiteConfig from app.models import ScoDocSiteConfig
from app.models.etudiants import Identite
from app.models.moduleimpls import ModuleImpl from app.models.moduleimpls import ModuleImpl
from app.models.ues import UniteEns from app.models.ues import UniteEns
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc.sco_codes_parcours import UE_SPORT from app.scodoc.sco_codes_parcours import UE_SPORT
import app.scodoc.sco_utils as scu
class ResultatsSemestreBUT(NotesTableCompat): class ResultatsSemestreBUT(NotesTableCompat):
@ -32,6 +34,10 @@ class ResultatsSemestreBUT(NotesTableCompat):
def __init__(self, formsemestre): def __init__(self, formsemestre):
super().__init__(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(): if not self.load_cached():
t0 = time.time() t0 = time.time()
@ -145,15 +151,228 @@ class ResultatsSemestreBUT(NotesTableCompat):
""" """
return self.modimpl_coefs_df.loc[ue.id].sum() 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 """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 coefs = self.modimpl_coefs_df # row UE, cols modimpl
return [ modimpls = [
modimpl modimpl
for modimpl in self.formsemestre.modimpls_sorted for modimpl in self.formsemestre.modimpls_sorted
if (coefs[modimpl.id][ue_id] != 0) if (coefs[modimpl.id][ue_id] != 0)
and self.modimpl_inscr_df[modimpl.id][etudid] 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])) r.append("-".join([x.lower().capitalize() for x in fields]))
return " ".join(r) 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 @cached_property
def sort_key(self) -> tuple: def sort_key(self) -> tuple:
"clé pour tris par ordre alphabétique" "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 numero = db.Column(db.Integer) # ordre de présentation
# id de l'element pedagogique Apogee correspondant: # id de l'element pedagogique Apogee correspondant:
code_apogee = db.Column(db.String(APO_CODE_STR_LEN)) 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") module_type = db.Column(db.Integer, nullable=False, default=0, server_default="0")
# Relations: # Relations:
modimpls = db.relationship("ModuleImpl", backref="module", lazy="dynamic") modimpls = db.relationship("ModuleImpl", backref="module", lazy="dynamic")
@ -76,6 +76,11 @@ class Module(db.Model):
def type_name(self): def type_name(self):
return scu.MODULE_TYPE_NAMES[self.module_type] 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: def set_ue_coef(self, ue, coef: float) -> None:
"""Set coef module vers cette UE""" """Set coef module vers cette UE"""
self.update_ue_coef_dict({ue.id: coef}) self.update_ue_coef_dict({ue.id: coef})

View File

@ -4,7 +4,6 @@
from app import db from app import db
from app.models import APO_CODE_STR_LEN from app.models import APO_CODE_STR_LEN
from app.models import SHORT_STR_LEN from app.models import SHORT_STR_LEN
from app.scodoc import notesdb as ndb
from app.scodoc import sco_utils as scu 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 import log
from app.but import bulletin_but from app.but import bulletin_but
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_but import ResultatsSemestreBUT
from app.comp.res_common import NotesTableCompat from app.comp.res_common import NotesTableCompat
from app.models import FormSemestre from app.models import FormSemestre
from app.models.etudiants import Identite from app.models.etudiants import Identite
@ -108,7 +109,7 @@ def formsemestre_recapcomplet(
page_title="Récapitulatif", page_title="Récapitulatif",
no_side_bar=True, no_side_bar=True,
init_qtip=True, init_qtip=True,
javascripts=["js/etud_info.js"], javascripts=["js/etud_info.js", "js/table_recap.js"],
), ),
sco_formsemestre_status.formsemestre_status_head( sco_formsemestre_status.formsemestre_status_head(
formsemestre_id=formsemestre_id formsemestre_id=formsemestre_id
@ -223,19 +224,28 @@ def do_formsemestre_recapcomplet(
force_publishing=True, force_publishing=True,
): ):
"""Calcule et renvoie le tableau récapitulatif.""" """Calcule et renvoie le tableau récapitulatif."""
data, filename, format = make_formsemestre_recapcomplet( formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
formsemestre_id=formsemestre_id, if (
format=format, formsemestre.formation.is_apc()
hidemodules=hidemodules, and format not in ("xml", "json")
hidebac=hidebac, and not modejury
xml_nodate=xml_nodate, ):
modejury=modejury, data, filename = make_formsemestre_recapcomplet_apc(formsemestre, format=format)
sortcol=sortcol, else:
xml_with_decisions=xml_with_decisions, data, filename, format = make_formsemestre_recapcomplet(
disable_etudlink=disable_etudlink, formsemestre_id=formsemestre_id,
rank_partition_id=rank_partition_id, format=format,
force_publishing=force_publishing, 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": if format == "xml" or format == "html":
return data return data
elif format == "csv": elif format == "csv":
@ -1004,3 +1014,59 @@ def formsemestres_bulletins(annee_scolaire):
jslist.append(J) jslist.append(J)
return scu.sendJSON(jslist) 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 RESSOURCE = 2 # BUT
SAE = 3 # 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 = { MODULE_TYPE_NAMES = {
ModuleType.STANDARD: "Module", ModuleType.STANDARD: "Module",

View File

@ -3238,3 +3238,45 @@ table.dataTable tr.gt_lastrow th {
table.dataTable td.etudinfo, table.dataTable td.group { table.dataTable td.etudinfo, table.dataTable td.group {
text-align: left; 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 -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.1.86" SCOVERSION = "9.2-86"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"