Tableau bilan UEs. Implements #989

This commit is contained in:
Emmanuel Viennet 2024-09-14 16:42:12 +02:00
parent 786fb039cf
commit cc1d9c03c3
8 changed files with 332 additions and 23 deletions

View File

@ -201,7 +201,7 @@ class UniteEns(models.ScoDocModel):
m.modimpls.all() for m in self.modules
)
def guess_semestre_idx(self) -> None:
def guess_semestre_idx(self) -> int:
"""Lorsqu'on prend une ancienne formation non APC,
les UE n'ont pas d'indication de semestre.
Cette méthode fixe le semestre en prenant celui du premier module,
@ -215,6 +215,7 @@ class UniteEns(models.ScoDocModel):
self.semestre_idx = module.semestre_id
db.session.add(self)
db.session.commit()
return self.semestre_idx
def get_ects(self, parcour: ApcParcours = None, only_parcours=False) -> float:
"""Crédits ECTS associés à cette UE.

View File

@ -456,6 +456,11 @@ def formsemestre_status_menubar(formsemestre: FormSemestre | None) -> str:
formsemestre_id
),
},
{
"title": "Bilan des UEs (expérimental)",
"endpoint": "notes.formsemestre_bilan_ues",
"args": {"formsemestre_id": formsemestre_id},
},
]
menu_stats = _build_menu_stats(formsemestre)

View File

@ -4511,7 +4511,7 @@ table.table_recap td.cursus_BUT1 {
}
table.table_recap td.cursus_BUT2 {
color: #d39f00;
color: #24848e;
}
table.table_recap td.cursus_BUT3 {
@ -4632,7 +4632,8 @@ table.table_recap tr.selected td.moy_ue_valid {
}
table.table_recap td.moy_ue_warning,
table.table_recap tr.selected td.moy_ue_warning {
table.table_recap tr.selected td.moy_ue_warning,
td.moy_ue_warning {
color: rgb(255, 0, 0);
}

196
app/tables/bilan_ues.py Normal file
View File

@ -0,0 +1,196 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Table bilan UEs des étudiants d'un formsemestre
Voir https://git.scodoc.org/ScoDoc/ScoDoc/issues/989
"""
from functools import partial
from collections import defaultdict
from app.models import (
Formation,
FormSemestre,
Identite,
ScolarFormSemestreValidation,
UniteEns,
)
from app.scodoc import sco_formsemestre_validation
from app.scodoc.codes_cursus import BUT_CODES_ORDER
from app.scodoc import sco_utils as scu
from app.tables.list_etuds import TableEtud, RowEtudWithInfos
def _acronym_ues_same_key(ues: set[UniteEns]) -> str:
"""Les UEs de même code pouvant (rarement, espérons) avoir des acronymes différents,
les regroupe dans une chaîne.
"""
# Note: on peut former un ensemble d'UE, voir
# https://stackoverflow.com/questions/8179068/sqlalchemy-id-equality-vs-reference-equality
acronymes = sorted({ue.acronyme for ue in ues})
return acronymes[0] + (
" (" + ", ".join(acronymes[1:]) + ")" if len(acronymes) > 1 else ""
)
def _best_validation(
validations: list[ScolarFormSemestreValidation],
) -> ScolarFormSemestreValidation | None:
"""Retourne le code jury de la meilleure validation
(au sens du code jury puis de la note moyenne)
"""
if not validations:
return None
best_validation = max(
validations, key=lambda v: (BUT_CODES_ORDER.get(v.code, -1), v.moy_ue or 0.0)
)
return best_validation
def get_etud_ue_validations_by_ue_key(
etud: Identite, formation: Formation
) -> dict[str | tuple[int, int], list[ScolarFormSemestreValidation]]:
"""Retourne les validations d'UEs pour un étudiant dans une formation.
Considère toutes les formations de même code que celle indiquée, puis regroupe les validations
par clé d'UE:
- en formations classiques, `ue.ue_code`
- en APC tuple `(ue.semestre_idx, ue.niveau_id)`
"""
validations = sco_formsemestre_validation.get_etud_ue_validations(etud, formation)
etud_ue_validations_by_ue_key = defaultdict(list)
if formation.is_apc():
for v in validations:
etud_ue_validations_by_ue_key[
(v.ue.semestre_idx, v.ue.niveau_competence_id)
].append(v)
else:
for v in validations:
etud_ue_validations_by_ue_key[v.ue.ue_code].append(v)
return etud_ue_validations_by_ue_key
class TableBilanUEs(TableEtud):
"""Table listant des étudiants avec bilan UEs"""
def __init__(
self,
formsemestre: FormSemestre,
convert_values=True,
classes: list[str] = None,
row_class=None,
with_foot_titles=False,
**kwargs,
):
self.formsemestre = formsemestre
etuds = formsemestre.etuds
if convert_values:
self.fmt_note = scu.fmt_note
else:
self.fmt_note = partial(scu.fmt_note, keep_numeric=True)
cursus = formsemestre.formation.get_cursus()
self.barre_moy = cursus.BARRE_MOY - scu.NOTES_TOLERANCE
self.is_apc = formsemestre.formation.is_apc()
self.validations_by_ue_key_by_etudid: dict[
int, dict[str | tuple[int, int] : list[ScolarFormSemestreValidation]]
] = {}
"validations d'UEs par étudiant, indicées par clé d'UE (code ou sem/niv. de comp.)"
self.ues_by_key: defaultdict[str | tuple[int, int] : set[UniteEns]] = (
defaultdict(set)
)
"ensemble des UEs associées à chaque clé"
for etud in etuds:
self.validations_by_ue_key_by_etudid[etud.id] = (
get_etud_ue_validations_by_ue_key(etud, formsemestre.formation)
)
# Le titre de chaque clé d'UE
for key, validations in self.validations_by_ue_key_by_etudid[
etud.id
].items():
self.ues_by_key[key].update({v.ue for v in validations})
# Titres des UEs
self.ue_titles = {
key: _acronym_ues_same_key(ues) for key, ues in self.ues_by_key.items()
}
# prend une UE pour chaque clé pour déterminer l'ordre des UEs
ue_by_key = {key: next(iter(ues)) for key, ues in self.ues_by_key.items()}
self.sorted_ues_keys = sorted(
self.ue_titles.keys(),
key=lambda k: (
ue_by_key[k].guess_semestre_idx(),
ue_by_key[k].numero,
),
)
"clés d'UEs triées par semestre/numero"
super().__init__(
etuds=etuds,
classes=classes or ["gt_table", "gt_left", "with-highlight", "table_recap"],
row_class=row_class or RowEtudWithUEs,
with_foot_titles=with_foot_titles,
**kwargs,
)
def add_etuds(self, etuds: list[Identite]):
"Ajoute des étudiants à la table"
for etud in etuds:
row = self.row_class(
self,
etud,
self.formsemestre,
self.formsemestre.etuds_inscriptions.get(etud.id),
)
row.add_etud_cols()
self.add_row(row)
class RowEtudWithUEs(RowEtudWithInfos):
"""Ligne de la table d'étudiants avec colonnes UEs"""
with_boursier = False # inutile dans cette table
with_departement = False
with_formsemestre = False
with_ine = True
def add_etud_cols(self):
"""Ajoute colonnes pour cet étudiant"""
super().add_etud_cols()
# Ajoute les colonnes d'UEs
for key in self.table.sorted_ues_keys:
self.add_etud_ue_cols(key)
def add_etud_ue_cols(self, key):
"colonnes pour une clé d'UE pour l'étudiant"
validations = self.table.validations_by_ue_key_by_etudid[self.etud.id].get(
key, []
)
if not validations:
return
best_validation = _best_validation(validations)
moy_ue = best_validation.moy_ue
note_class = ""
if moy_ue is None:
moy_ue = scu.NO_NOTE_STR
if isinstance(moy_ue, float) and moy_ue < self.table.barre_moy:
note_class = "moy_ue_warning" # en rouge
self.add_cell(
str(key) + "_note",
self.table.ue_titles[key],
self.table.fmt_note(moy_ue),
data={"order": moy_ue},
raw_content=moy_ue,
classes=[note_class],
column_classes={"note_ue"},
group=f"col_s{best_validation.ue.semestre_idx}",
)
if self.table.is_apc:
class_cursus = f"cursus_BUT{(best_validation.ue.semestre_idx+1)//2}"
else:
class_cursus = ""
self.add_cell(
str(key) + "_code",
self.table.ue_titles[key],
best_validation.code,
column_classes={"code_ue", class_cursus},
group=f"col_s{best_validation.ue.semestre_idx}",
)

View File

@ -6,7 +6,6 @@
"""Liste simple d'étudiants
"""
import datetime
from app.models import FormSemestre, FormSemestreInscription, Identite
from app.scodoc.sco_exceptions import ScoValueError
@ -131,6 +130,11 @@ class RowEtudWithInfos(RowEtud):
département, formsemestre, codes, boursier
"""
with_boursier = True
with_departement = True
with_formsemestre = True
with_ine = False
def __init__(
self,
table: TableEtud,
@ -146,17 +150,25 @@ class RowEtudWithInfos(RowEtud):
def add_etud_cols(self):
"""Ajoute colonnes étudiant: codes, noms"""
self.add_cell("dept", "Dépt.", self.etud.departement.acronym, "identite_detail")
self.add_cell(
"formsemestre",
"Semestre",
f"""{self.formsemestre.titre_formation()} {
('S'+str(self.formsemestre.semestre_id))
if self.formsemestre.semestre_id > 0 else ''}
""",
"identite_detail",
)
if self.with_departement:
self.add_cell(
"dept", "Dépt.", self.etud.departement.acronym, "identite_detail"
)
if self.with_formsemestre:
self.add_cell(
"formsemestre",
"Semestre",
f"""{self.formsemestre.titre_formation()} {
('S'+str(self.formsemestre.semestre_id))
if self.formsemestre.semestre_id > 0 else ''}
""",
"identite_detail",
)
self.add_cell("code_nip", "NIP", self.etud.code_nip or "", "identite_detail")
if self.with_ine:
self.add_cell(
"code_ine", "INE", self.etud.code_ine or "", "identite_detail"
)
super().add_etud_cols()
self.add_cell(
"etat",
@ -164,12 +176,13 @@ class RowEtudWithInfos(RowEtud):
self.inscription.etat,
"inscription",
)
self.add_cell(
"boursier",
"Boursier",
"O" if self.etud.boursier else "N",
"identite_infos",
)
if self.with_boursier:
self.add_cell(
"boursier",
"Boursier",
"O" if self.etud.boursier else "N",
"identite_infos",
)
class TableEtudWithInfos(TableEtud):

View File

@ -0,0 +1,64 @@
{# Table bilan résultats d'UEs #}
{% extends "sco_page.j2" %}
{% block styles %}
{{ super() }}
<style>
td.ue_s1 {
color: red;
}
/* bordures entre semestres */
table.gt_table td.col_s1 + td.col_s2,
table.gt_table td.col_s2 + td.col_s3,
table.gt_table td.col_s3 + td.col_s4,
table.gt_table td.col_s4 + td.col_s5,
table.gt_table td.col_s5 + td.col_s6,
table.gt_table td.col_s6 + td.col_s7,
table.gt_table td.col_s7 + td.col_s8,
table.gt_table td.col_s8 + td.col_s9,
table.gt_table td.col_s9 + td.col_s10 {
border-left: 2px solid rgb(193, 165, 165); /* or border-right, depending on layout */
}
table.gt_table td {
border-bottom: 1px solid rgb(193, 165, 165);
}
table.dataTable tbody tr:nth-child(odd),
table.dataTable tbody tr:nth-child(odd) td.dtfc-fixed-start {
background-color: rgb(226, 241, 243);
}
table.dataTable tbody tr:nth-child(even),
table.dataTable tbody tr:nth-child(even) td.dtfc-fixed-start {
background-color: rgb(253, 255, 240);
}
</style>
{% endblock styles %}
{% block app_content %}
<div class="pageContent">
<h2>Bilan des UEs validées dans leur cursus par les étudiants du semestre</h2>
<div class="scobox warning">page expérimentale, à utiliser avec précaution, merci de commenter sur le
<a class="stdlink" noreferer href="{{scu.SCO_DISCORD_ASSISTANCE|safe}}">{{scu.SCO_DISCORD_ASSISTANCE}}</a>.
</div>
<div>
{{table.html()|safe}}
</div>
<a title="export excel" href="{{
url_for('notes.formsemestre_bilan_ues', scodoc_dept=g.scodoc_dept, formsemestre_id=sco.formsemestre.id, fmt='xls')
}}">{{scu.ICON_XLS|safe}}</a>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script src="{{scu.STATIC_DIR}}/js/groups_view.js"></script>
{% endblock %}

View File

@ -74,8 +74,7 @@ from app.scodoc.sco_exceptions import (
)
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_pv_dict import descr_autorisations
# from app.scodoc.TrivialFormulator import TrivialFormulator
from app.tables import bilan_ues
from app.views import notes_bp as bp
from app.views import ScoData
@ -1032,3 +1031,33 @@ def etud_bilan_ects(etudid: int):
validations_by_diplome=validations_by_diplome,
sco=ScoData(etud=etud),
)
@bp.route("/bilan_ues/<int:formsemestre_id>")
@scodoc
@permission_required(Permission.ScoView)
def formsemestre_bilan_ues(formsemestre_id: int):
"""Table bilan validations UEs pour les étudiants du semestre"""
fmt = request.args.get("fmt", "html")
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
table = bilan_ues.TableBilanUEs(formsemestre)
if fmt.startswith("xls"):
return scu.send_file(
table.excel(),
scu.make_filename(
f"""{formsemestre.titre_num()}-bilan-ues-{
datetime.datetime.now().strftime("%Y-%m-%dT%Hh%M")}"""
),
scu.XLSX_SUFFIX,
mime=scu.XLSX_MIMETYPE,
)
if fmt == "html":
return render_template(
"jury/formsemestre_bilan_ues.j2",
sco=ScoData(formsemestre=formsemestre),
table=table,
title=f"Bilan UEs {formsemestre.titre_num()}",
)
else:
raise ScoValueError("invalid fmt value")

View File

@ -1,7 +1,7 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
SCOVERSION = "9.7.21"
SCOVERSION = "9.7.22"
SCONAME = "ScoDoc"