diff --git a/app/models/ues.py b/app/models/ues.py index e5aa8445e..5c458620b 100644 --- a/app/models/ues.py +++ b/app/models/ues.py @@ -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. diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index c530bd175..93c382db5 100755 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -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) diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index d27fa8a9e..56e76e299 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -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); } diff --git a/app/tables/bilan_ues.py b/app/tables/bilan_ues.py new file mode 100644 index 000000000..ac73c8d67 --- /dev/null +++ b/app/tables/bilan_ues.py @@ -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}", + ) diff --git a/app/tables/list_etuds.py b/app/tables/list_etuds.py index 463295d93..714cf29fb 100644 --- a/app/tables/list_etuds.py +++ b/app/tables/list_etuds.py @@ -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): diff --git a/app/templates/jury/formsemestre_bilan_ues.j2 b/app/templates/jury/formsemestre_bilan_ues.j2 new file mode 100644 index 000000000..fe846dd6f --- /dev/null +++ b/app/templates/jury/formsemestre_bilan_ues.j2 @@ -0,0 +1,64 @@ +{# Table bilan résultats d'UEs #} + +{% extends "sco_page.j2" %} + +{% block styles %} + {{ super() }} + + +{% endblock styles %} + +{% block app_content %} +