Skip to content
Snippets Groups Projects
Select Git revision
  • fca23bbec8261b6193099196cff79ea354397c49
  • main default protected
  • v5.2
  • v5.1
  • v7.1
  • v7
  • v6.2
  • v6.1
  • v6
  • v5.9
  • v5.8
  • v5.7
  • v5.6
  • v5.5
  • v5
  • v5.3
  • v4.6
  • v4.6-problem
  • v4.5
  • v4
  • v3.2
  • v3.1
22 results

personListingController.component.jsx

Blame
  • Forked from javascript / intro-react
    Source project has a limited visibility.
    res_compat.py 21.84 KiB
    ##############################################################################
    # ScoDoc
    # Copyright (c) 1999 - 2024 Emmanuel Viennet.  All rights reserved.
    # See LICENSE
    ##############################################################################
    
    """Classe résultats pour compatibilité avec le code ScoDoc 7
    """
    from functools import cached_property
    import pandas as pd
    
    from flask import flash, g, url_for
    from markupsafe import Markup
    
    from app import db, log
    from app.comp import moy_sem
    from app.comp.aux_stats import StatsMoyenne
    from app.comp.res_common import ResultatsSemestre
    from app.models import (
        Evaluation,
        Identite,
        FormSemestre,
        ModuleImpl,
        ScolarAutorisationInscription,
    )
    from app.scodoc.codes_cursus import UE_SPORT, DEF
    from app.scodoc import sco_utils as scu
    
    
    # Pour raccorder le code des anciens codes qui attendent une NoteTable
    class NotesTableCompat(ResultatsSemestre):
        """Implementation partielle de NotesTable
    
        Les méthodes définies dans cette classe sont là
        pour conserver la compatibilité avec les codes anciens et
        il n'est pas recommandé de les utiliser dans de nouveaux
        développements (API malcommode et peu efficace).
        """
    
        _cached_attrs = ResultatsSemestre._cached_attrs + (
            "malus",
            "etud_moy_gen_ranks",
            "etud_moy_gen_ranks_int",
            "moy_gen_rangs_by_group",
            "ue_rangs",
            "ue_rangs_by_group",
        )
    
        def __init__(self, formsemestre: FormSemestre):
            super().__init__(formsemestre)
    
            nb_etuds = len(self.etuds)
            self.ue_rangs = {u.id: (None, nb_etuds) for u in self.ues}
            self.mod_rangs = None  # sera surchargé en Classic, mais pas en APC
            """{ modimpl_id : (rangs, effectif) }"""
            self.moy_min = "NA"
            self.moy_max = "NA"
            self.moy_moy = "NA"
            self.moy_gen_rangs_by_group = {}  # { group_id : (Series, Series) }
            self.ue_rangs_by_group = {}  # { ue_id : {group_id : (Series, Series)}}
            self.parcours = self.formsemestre.formation.get_cursus()
            self._modimpls_dict_by_ue = {}  # local cache
    
        def get_inscrits(self, include_demdef=True, order_by=False) -> list[Identite]:
            """Liste des étudiants inscrits
            order_by = False|'nom'|'moy' tri sur nom ou sur moyenne générale (indicative)
    
            Note: pour récupérer les etudids des inscrits, non triés, il est plus efficace
            d'utiliser `[ ins.etudid for ins in nt.formsemestre.inscriptions ]`
            """
            etuds = self.formsemestre.get_inscrits(
                include_demdef=include_demdef, order=(order_by == "nom")
            )
            if order_by == "moy":
                etuds.sort(
                    key=lambda e: (
                        self.etud_moy_gen_ranks_int.get(e.id, 100000),
                        e.sort_key,
                    )
                )
            return etuds
    
        def get_etudids(self) -> list[int]:
            """(deprecated)
            Liste des etudids inscrits, incluant les démissionnaires.
            triée par ordre alphabetique de NOM
            (à éviter: renvoie les etudids, mais est moins efficace que get_inscrits)
            """
            # Note: pour avoir les inscrits non triés,
            # utiliser [ ins.etudid for ins in self.formsemestre.inscriptions ]
            return [x["etudid"] for x in self.inscrlist]
    
        @cached_property
        def sem(self) -> dict:
            """le formsemestre, comme un gros et gras dict (nt.sem)"""
            return self.formsemestre.get_infos_dict()
    
        @cached_property
        def inscrlist(self) -> list[dict]:  # utilisé par PE
            """Liste des inscrits au semestre (avec DEM et DEF),
            sous forme de dict etud,
            classée dans l'ordre alphabétique de noms.
            """
            etuds = self.formsemestre.get_inscrits(include_demdef=True, order=True)
            return [e.to_dict_scodoc7() for e in etuds]
    
        @cached_property
        def stats_moy_gen(self):
            """Stats (moy/min/max) sur la moyenne générale"""
            return StatsMoyenne(self.etud_moy_gen)
    
        def get_ues_stat_dict(self, filter_sport=False, check_apc_ects=True) -> list[dict]:
            """Liste des UEs de toutes les UEs du semestre (tous parcours),
            ordonnée par numero.
            Si filter_sport, retire les UE de type SPORT.
            Résultat: liste de dicts { champs UE U stats moyenne UE }
            """
            ues = self.formsemestre.get_ues(with_sport=not filter_sport)
            ues_dict = []
            for ue in ues:
                d = ue.to_dict()
                if ue.type != UE_SPORT:
                    moys = self.etud_moy_ue[ue.id]
                else:
                    moys = None
                d.update(StatsMoyenne(moys).to_dict())
                ues_dict.append(d)
            if check_apc_ects and self.is_apc and not hasattr(g, "checked_apc_ects"):
                g.checked_apc_ects = True
                if None in [ue.ects for ue in ues if ue.type != UE_SPORT]:
                    formation = self.formsemestre.formation
                    ue_sans_ects = [
                        ue for ue in ues if ue.type != UE_SPORT and ue.ects is None
                    ]
                    flash(
                        Markup(
                            f"""Calcul moyenne générale impossible: ECTS des UE manquants !<br>
                        (dans {' ,'.join([ue.acronyme for ue in ue_sans_ects])}
                        de la formation: <a href="{url_for("notes.ue_table",
                        scodoc_dept=g.scodoc_dept, formation_id=formation.id)
                        }">{formation.get_titre_version()}</a>)
                        )
                        """
                        ),
                        category="danger",
                    )
            return ues_dict
    
        def get_modimpls_dict(self, ue_id=None) -> list[dict]:
            """Liste des modules pour une UE (ou toutes si ue_id==None),
            triés par numéros (selon le type de formation)
            """
            # cached ?
            modimpls_dict = self._modimpls_dict_by_ue.get(ue_id)
            if modimpls_dict:
                return modimpls_dict
            modimpls_dict = []
            for modimpl in self.formsemestre.modimpls_sorted:
                if (ue_id is None) or (modimpl.module.ue.id == ue_id):
                    d = modimpl.to_dict()
                    # compat ScoDoc < 9.2: ajoute matières
                    d["mat"] = modimpl.module.matiere.to_dict()
                    modimpls_dict.append(d)
            self._modimpls_dict_by_ue[ue_id] = modimpls_dict
            return modimpls_dict
    
        def compute_rangs(self):
            """Calcule les classements
            Moyenne générale: etud_moy_gen_ranks
            Par UE (sauf ue bonus): ue_rangs[ue.id]
            Par groupe: classements selon moy_gen et UE:
                moy_gen_rangs_by_group[group_id]
                ue_rangs_by_group[group_id]
            """
            mask_inscr = pd.Series(
                [
                    self.formsemestre.etuds_inscriptions[etudid].etat == scu.INSCRIT
                    for etudid in self.etud_moy_gen.index
                ],
                dtype=float,
                index=self.etud_moy_gen.index,
            )
            etud_moy_gen_dem_zero = self.etud_moy_gen * mask_inscr
            (
                self.etud_moy_gen_ranks,
                self.etud_moy_gen_ranks_int,
            ) = moy_sem.comp_ranks_series(etud_moy_gen_dem_zero)
            ues = self.formsemestre.get_ues()
            for ue in ues:
                moy_ue = self.etud_moy_ue[ue.id]
                self.ue_rangs[ue.id] = (
                    moy_sem.comp_ranks_series(moy_ue * mask_inscr)[0],  # juste en chaine
                    int(moy_ue.count()),
                )
                # .count() -> nb of non NaN values
            # Rangs dans les groupes (moy. gen et par UE)
            self.moy_gen_rangs_by_group = {}  # { group_id : (Series, Series) }
            self.ue_rangs_by_group = {}
            partitions_avec_rang = self.formsemestre.partitions.filter_by(
                bul_show_rank=True
            )
            for partition in partitions_avec_rang:
                for group in partition.groups:
                    # on prend l'intersection car les groupes peuvent inclure des étudiants désinscrits
                    group_members = list(
                        {etud.id for etud in group.etuds}.intersection(
                            self.etud_moy_gen.index
                        )
                    )
                    # list() car pandas veut une sequence pour take()
                    # Rangs / moyenne générale:
                    group_moys_gen = etud_moy_gen_dem_zero[group_members]
                    self.moy_gen_rangs_by_group[group.id] = moy_sem.comp_ranks_series(
                        group_moys_gen
                    )
                    # Rangs / UEs:
                    for ue in ues:
                        group_moys_ue = self.etud_moy_ue[ue.id][group_members]
                        self.ue_rangs_by_group.setdefault(ue.id, {})[group.id] = (
                            moy_sem.comp_ranks_series(group_moys_ue * mask_inscr)
                        )
    
        def get_etud_rang(self, etudid: int) -> str:
            """Le rang (classement) de l'étudiant dans le semestre.
            Result: "13" ou "12 ex"
            """
            return self.etud_moy_gen_ranks.get(etudid, 99999)
    
        def get_etud_ue_rang(self, ue_id, etudid, group_id=None) -> tuple[str, int]:
            """Le rang de l'étudiant dans cette ue
            Si le group_id est spécifié, rang au sein de ce groupe, sinon global.
            Result: rang:str, effectif:str
            """
            if group_id is None:
                rangs, effectif = self.ue_rangs[ue_id]
                if rangs is not None:
                    rang = rangs[etudid]
                else:
                    return "", ""
            else:
                rangs = self.ue_rangs_by_group[ue_id][group_id][0]
                rang = rangs[etudid]
                effectif = len(rangs)
            return rang, effectif
    
        def get_etud_rang_group(self, etudid: int, group_id: int) -> tuple[str, int]:
            """Rang de l'étudiant (selon moy gen) et effectif dans ce groupe.
            Si le groupe n'a pas de rang (partition avec bul_show_rank faux), ramène "", 0
            """
            if group_id in self.moy_gen_rangs_by_group:
                r = self.moy_gen_rangs_by_group[group_id][0]  # version en str
                return (r[etudid], len(r))
            else:
                return "", 0
    
        def etud_check_conditions_ues(self, etudid):
            """Vrai si les conditions sur les UE sont remplies.
            Ne considère que les UE ayant des notes (moyenne calculée).
            (les UE sans notes ne sont pas comptées comme sous la barre)
            Prend en compte les éventuelles UE capitalisées.
    
            Pour les parcours habituels, cela revient à vérifier que
            les moyennes d'UE sont toutes > à leur barre (sauf celles sans notes)
    
            Pour les parcours non standards (LP2014), cela peut être plus compliqué.
    
            Return: True|False, message explicatif
            """
            ue_status_list = []
            for ue in self.formsemestre.get_ues():
                ue_status = self.get_etud_ue_status(etudid, ue.id)
                if ue_status:
                    ue_status_list.append(ue_status)
            return self.parcours.check_barre_ues(ue_status_list)
    
        def etudids_without_decisions(self) -> list[int]:
            """Liste des id d'étudiants du semestre non démissionnaires
            n'ayant pas de décision de jury.
            - En classic: ne regarde pas que la décision de semestre (pas les décisions d'UE).
            - en BUT: utilise etud_has_decision
            """
            check_func = (
                self.etud_has_decision if self.is_apc else self.get_etud_decision_sem
            )
            etudids = [
                ins.etudid
                for ins in self.formsemestre.inscriptions
                if (ins.etat == scu.INSCRIT) and (not check_func(ins.etudid))
            ]
            return etudids
    
        def etud_has_decision(self, etudid, include_rcues=True) -> bool:
            """True s'il y a une décision de jury pour cet étudiant émanant de ce formsemestre.
            prend aussi en compte les autorisations de passage.
            Si include_rcues, prend en compte les validation d'RCUEs en BUT (pas d'effet en classic).
            Sous-classée en BUT pour les RCUEs et années.
            """
            return bool(
                self.get_etud_decisions_ue(etudid)
                or self.get_etud_decision_sem(etudid, ignore_def=True)
                or ScolarAutorisationInscription.query.filter_by(
                    origin_formsemestre_id=self.formsemestre.id, etudid=etudid
                ).count()
            )
    
        def get_etud_decisions_ue(self, etudid: int) -> dict:
            """Decisions du jury pour les UE de cet etudiant dans ce formsemestre,
            ou None s'il n'y en pas eu.
            Ne tient pas compte des UE capitalisées ou externes.
            { ue_id : {
                'code' : ADM|CMP|AJ|ADSUP|...,
                'event_date' : "d/m/y",
                'ects' :  float, nb d'ects validées dans l'UE de ce semestre.
                }
                ...
            }
            Ne renvoie aucune decision d'UE pour les défaillants
            """
            if self.get_etud_etat(etudid) == DEF:
                return {}
            else:
                validations = self.get_formsemestre_validations()
                return validations.decisions_jury_ues.get(etudid, None)
    
        def get_etud_ects_valides(self, etudid: int, decisions_ues: dict = False) -> float:
            """Le total des ECTS validés (et enregistrés) par l'étudiant dans ce semestre.
            NB: avant jury, rien d'enregistré, donc zéro ECTS.
            Optimisation: si decisions_ues est passé, l'utilise, sinon appelle get_etud_decisions_ue()
            """
            if decisions_ues is False:
                decisions_ues = self.get_etud_decisions_ue(etudid)
            if not decisions_ues:
                return 0.0
            return float(sum(d.get("ects", 0) for d in decisions_ues.values()))
    
        def get_etud_decision_sem(self, etudid: int, ignore_def: bool = False) -> dict:
            """Decision du jury semestre prise pour cet etudiant, ou None s'il n'y en pas eu.
            { 'code' : None|ATT|..., 'assidu' : 0|1, 'event_date' : , compense_formsemestre_id }
            Si état défaillant, force le code a DEF, sauf si ignore_def=True.
            Normalement None en BUT, sauf si DEF.
            """
            if (not ignore_def) and self.get_etud_etat(etudid) == DEF:
                return {
                    "code": DEF,
                    "assidu": False,
                    "event_date": "",
                    "compense_formsemestre_id": None,
                }
            validations = self.get_formsemestre_validations()
            return validations.decisions_jury.get(etudid, None)
    
        def get_etud_mat_moy(self, matiere_id: int, etudid: int) -> str:
            """moyenne d'un étudiant dans une matière (ou NA si pas de notes)"""
            if not self.moyennes_matieres:
                return "nd"
            return (
                self.moyennes_matieres[matiere_id].get(etudid, "-")
                if matiere_id in self.moyennes_matieres
                else "-"
            )
    
        def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float:
            """La moyenne de l'étudiant dans le moduleimpl
            En APC, il s'agira d'une moyenne indicative sans valeur.
            Result: valeur float (peut être NaN) ou chaîne "NI" (non inscrit ou DEM)
            """
            raise NotImplementedError()  # virtual method
    
        def get_etud_moy_gen(self, etudid):  # -> float | str
            """Moyenne générale de cet etudiant dans ce semestre.
            Prend en compte les UE capitalisées.
            Si apc, moyenne indicative.
            Si pas de notes: 'NA'
            """
            return self.etud_moy_gen[etudid]
    
        def get_etud_ects_pot(self, etudid: int) -> dict:
            """
            Un dict avec les champs
             ects_pot : (float) nb de crédits ECTS qui seraient validés
                        (sous réserve de validation par le jury)
             ects_pot_fond: (float) nb d'ECTS issus d'UE fondamentales (non électives)
             ects_total: (float) total des ECTS validables
    
            Les ects_pot sont les ECTS des UE au dessus de la barre (10/20 en principe),
            avant le jury (donc non encore enregistrés).
            """
            # was nt.get_etud_moy_infos
            # XXX pour compat nt, à remplacer ultérieurement
            ues = self.get_etud_ue_validables(etudid)
            ects_pot = 0.0
            ects_total = 0.0
            for ue in ues:
                if ue.id in self.etud_moy_ue and ue.ects is not None:
                    ects_total += ue.ects
                    if self.etud_moy_ue[ue.id][etudid] > self.parcours.NOTES_BARRE_VALID_UE:
                        ects_pot += ue.ects
            return {
                "ects_pot": ects_pot,
                "ects_pot_fond": 0.0,  # not implemented (anciennemment pour école ingé)
                "ects_total": ects_total,
            }
    
        def get_modimpl_evaluations_completes(self, moduleimpl_id: int) -> list[Evaluation]:
            """Liste d'informations (compat NotesTable) sur évaluations completes
            de ce module.
            Évaluation "complete" ssi toutes notes saisies ou en attente.
            """
            modimpl: ModuleImpl = db.session.get(ModuleImpl, moduleimpl_id)
            modimpl_results = self.modimpls_results.get(moduleimpl_id)
            if not modimpl_results:
                return []  # safeguard
            evaluations = []
            for e in modimpl.evaluations:
                if modimpl_results.evaluations_completes_dict.get(e.id, False):
                    evaluations.append(e)
                elif e.id not in modimpl_results.evaluations_completes_dict:
                    # ne devrait pas arriver ? XXX
                    log(
                        f"Warning: 220213 get_modimpl_evaluations_completes {e.id} not in mod {moduleimpl_id} ?"
                    )
            return evaluations
    
        def get_evaluations_etats(self) -> dict[int, dict]:
            """ "état" de chaque évaluation du semestre
            {
               evaluation_id : {
                    "evalcomplete" : bool,
                    "last_modif" : datetime | None
                    "nb_notes" : int,
                }, ...
            }
            """
            # utilisé par do_evaluation_etat_in_sem
            evaluations_etats = {}
            for modimpl in self.formsemestre.modimpls_sorted:
                for evaluation in modimpl.evaluations:
                    evaluation_etat = self.get_evaluation_etat(evaluation)
                    evaluations_etats[evaluation.id] = evaluation_etat["etat"]
            return evaluations_etats
    
        # ancienne version < 2024-02-02
        # def get_mod_evaluation_etat_list(self, moduleimpl_id) -> list[dict]:
        #     """Liste des états des évaluations de ce module
        #     ordonnée selon (numero desc, date_debut desc)
        #     """
        #     # à moderniser: lent, recharge des données que l'on a déjà...
        #     # remplacemé par ResultatsSemestre.get_mod_evaluation_etat_list
        #     #
        #     return [
        #         e
        #         for e in self.get_evaluations_etats()
        #         if e["moduleimpl_id"] == moduleimpl_id
        #     ]
    
        def get_moduleimpls_attente(self):
            """Liste des modimpls du semestre ayant des notes en attente"""
            return [
                modimpl
                for modimpl in self.formsemestre.modimpls_sorted
                if self.modimpls_results[modimpl.id].en_attente
            ]
    
        def get_mod_stats(self, moduleimpl_id: int) -> dict:
            """Stats sur les notes obtenues dans un modimpl
            Vide en APC
            """
            return {
                "moy": "-",
                "max": "-",
                "min": "-",
                "nb_notes": "-",
                "nb_missing": "-",
                "nb_valid_evals": "-",
            }
    
        def get_nom_short(self, etudid):
            "formatte nom d'un etud (pour table recap)"
            etud = self.identdict[etudid]
            return (
                (etud["nom_usuel"] or etud["nom"]).upper()
                + " "
                + etud["prenom"].capitalize()[:2]
                + "."
            )
    
        @cached_property
        def T(self):
            return self.get_table_moyennes_triees()
    
        def get_table_moyennes_triees(self) -> list:
            """Result: liste de tuples
            moy_gen, moy_ue_0, ..., moy_ue_n, moy_mod1, ..., moy_mod_n, etudid
            """
            table_moyennes = []
            etuds_inscriptions = self.formsemestre.etuds_inscriptions
            ues = self.formsemestre.get_ues(with_sport=True)  # avec bonus
            for etudid in etuds_inscriptions:
                moy_gen = self.etud_moy_gen.get(etudid, False)
                if moy_gen is False:
                    # pas de moyenne: démissionnaire ou def
                    t = (
                        ["-"]
                        + ["0.00"] * len(self.ues)
                        + ["NI"] * len(self.formsemestre.modimpls_sorted)
                    )
                else:
                    moy_ues = []
                    ue_is_cap = {}
                    for ue in ues:
                        ue_status = self.get_etud_ue_status(etudid, ue.id)
                        if ue_status:
                            moy_ues.append(ue_status["moy"])
                            ue_is_cap[ue.id] = ue_status["is_capitalized"]
                        else:
                            moy_ues.append("?")
                    t = [moy_gen] + list(moy_ues)
                    # Moyennes modules:
                    for modimpl in self.formsemestre.modimpls_sorted:
                        if ue_is_cap.get(modimpl.module.ue.id, False):
                            val = "-c-"
                        else:
                            val = self.get_etud_mod_moy(modimpl.id, etudid)
                        t.append(val)
                t.append(etudid)
                table_moyennes.append(t)
            # tri par moyennes décroissantes,
            # en laissant les démissionnaires à la fin, par ordre alphabetique
            etuds = [ins.etud for ins in etuds_inscriptions.values()]
            etuds.sort(key=lambda e: e.sort_key)
            self._rang_alpha = {e.id: i for i, e in enumerate(etuds)}
            table_moyennes.sort(key=self._row_key)
            return table_moyennes
    
        def _row_key(self, x):
            """clé de tri par moyennes décroissantes,
            en laissant les demissionnaires à la fin, par ordre alphabetique.
            (moy_gen, rang_alpha)
            """
            try:
                moy = -float(x[0])
            except (ValueError, TypeError):
                moy = 1000.0
            return (moy, self._rang_alpha[x[-1]])
    
        @cached_property
        def identdict(self) -> dict:
            """{ etudid : etud_dict } pour tous les inscrits au semestre"""
            return {
                ins.etud.id: ins.etud.to_dict_scodoc7()
                for ins in self.formsemestre.inscriptions
            }