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
|
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.
|
||||||
|
@ -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)
|
||||||
|
@ -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
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
|
"""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,17 +150,25 @@ 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(
|
self.add_cell(
|
||||||
"formsemestre",
|
"dept", "Dépt.", self.etud.departement.acronym, "identite_detail"
|
||||||
"Semestre",
|
)
|
||||||
f"""{self.formsemestre.titre_formation()} {
|
if self.with_formsemestre:
|
||||||
('S'+str(self.formsemestre.semestre_id))
|
self.add_cell(
|
||||||
if self.formsemestre.semestre_id > 0 else ''}
|
"formsemestre",
|
||||||
""",
|
"Semestre",
|
||||||
"identite_detail",
|
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")
|
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,12 +176,13 @@ class RowEtudWithInfos(RowEtud):
|
|||||||
self.inscription.etat,
|
self.inscription.etat,
|
||||||
"inscription",
|
"inscription",
|
||||||
)
|
)
|
||||||
self.add_cell(
|
if self.with_boursier:
|
||||||
"boursier",
|
self.add_cell(
|
||||||
"Boursier",
|
"boursier",
|
||||||
"O" if self.etud.boursier else "N",
|
"Boursier",
|
||||||
"identite_infos",
|
"O" if self.etud.boursier else "N",
|
||||||
)
|
"identite_infos",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TableEtudWithInfos(TableEtud):
|
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_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")
|
||||||
|
@ -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"
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user