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 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, """Lorsqu'on prend une ancienne formation non APC,
les UE n'ont pas d'indication de semestre. les UE n'ont pas d'indication de semestre.
Cette méthode fixe le semestre en prenant celui du premier module, 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 self.semestre_idx = module.semestre_id
db.session.add(self) db.session.add(self)
db.session.commit() db.session.commit()
return self.semestre_idx
def get_ects(self, parcour: ApcParcours = None, only_parcours=False) -> float: def get_ects(self, parcour: ApcParcours = None, only_parcours=False) -> float:
"""Crédits ECTS associés à cette UE. """Crédits ECTS associés à cette UE.

View File

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

View File

@ -4511,7 +4511,7 @@ table.table_recap td.cursus_BUT1 {
} }
table.table_recap td.cursus_BUT2 { table.table_recap td.cursus_BUT2 {
color: #d39f00; color: #24848e;
} }
table.table_recap td.cursus_BUT3 { 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 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); 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 """Liste simple d'étudiants
""" """
import datetime
from app.models import FormSemestre, FormSemestreInscription, Identite from app.models import FormSemestre, FormSemestreInscription, Identite
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
@ -131,6 +130,11 @@ class RowEtudWithInfos(RowEtud):
département, formsemestre, codes, boursier département, formsemestre, codes, boursier
""" """
with_boursier = True
with_departement = True
with_formsemestre = True
with_ine = False
def __init__( def __init__(
self, self,
table: TableEtud, table: TableEtud,
@ -146,7 +150,11 @@ class RowEtudWithInfos(RowEtud):
def add_etud_cols(self): def add_etud_cols(self):
"""Ajoute colonnes étudiant: codes, noms""" """Ajoute colonnes étudiant: codes, noms"""
self.add_cell("dept", "Dépt.", self.etud.departement.acronym, "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( self.add_cell(
"formsemestre", "formsemestre",
"Semestre", "Semestre",
@ -157,6 +165,10 @@ class RowEtudWithInfos(RowEtud):
"identite_detail", "identite_detail",
) )
self.add_cell("code_nip", "NIP", self.etud.code_nip or "", "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() super().add_etud_cols()
self.add_cell( self.add_cell(
"etat", "etat",
@ -164,6 +176,7 @@ class RowEtudWithInfos(RowEtud):
self.inscription.etat, self.inscription.etat,
"inscription", "inscription",
) )
if self.with_boursier:
self.add_cell( self.add_cell(
"boursier", "boursier",
"Boursier", "Boursier",

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_permissions import Permission
from app.scodoc.sco_pv_dict import descr_autorisations from app.scodoc.sco_pv_dict import descr_autorisations
from app.tables import bilan_ues
# from app.scodoc.TrivialFormulator import TrivialFormulator
from app.views import notes_bp as bp from app.views import notes_bp as bp
from app.views import ScoData from app.views import ScoData
@ -1032,3 +1031,33 @@ def etud_bilan_ects(etudid: int):
validations_by_diplome=validations_by_diplome, validations_by_diplome=validations_by_diplome,
sco=ScoData(etud=etud), 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 -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.7.21" SCOVERSION = "9.7.22"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"