############################################################################## # 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 ue_semestre_idx = best_validation.ue.guess_semestre_idx() 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{ue_semestre_idx}", ) if self.table.is_apc: class_cursus = f"cursus_BUT{(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{ue_semestre_idx}", )