forked from ScoDoc/ScoDoc
Tableau bilan UEs. Implements #989
This commit is contained in:
parent
786fb039cf
commit
cc1d9c03c3
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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
196
app/tables/bilan_ues.py
Normal 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}",
|
||||
)
|
@ -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):
|
||||
|
64
app/templates/jury/formsemestre_bilan_ues.j2
Normal file
64
app/templates/jury/formsemestre_bilan_ues.j2
Normal 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 %}
|
@ -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")
|
||||
|
@ -1,7 +1,7 @@
|
||||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
SCOVERSION = "9.7.21"
|
||||
SCOVERSION = "9.7.22"
|
||||
|
||||
SCONAME = "ScoDoc"
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user