Skip to content
Snippets Groups Projects
Select Git revision
  • 820e42ac28755fd85a5b9c940b1e69b611d10eea
  • master default protected
2 results

sco_version.py

Blame
  • Forked from Jean-Marie Place / SCODOC_R6A06
    Source project has a limited visibility.
    bulletin_but.py 21.51 KiB
    ##############################################################################
    # ScoDoc
    # Copyright (c) 1999 - 2023 Emmanuel Viennet.  All rights reserved.
    # See LICENSE
    ##############################################################################
    
    """Génération bulletin BUT
    """
    
    import collections
    import datetime
    import numpy as np
    from flask import g, has_request_context, url_for
    
    from app import db
    from app.comp.res_but import ResultatsSemestreBUT
    from app.models import Evaluation, FormSemestre, Identite
    from app.models.groups import GroupDescr
    from app.models.ues import UniteEns
    from app.scodoc import sco_bulletins, sco_utils as scu
    from app.scodoc import sco_bulletins_json
    from app.scodoc import sco_bulletins_pdf
    from app.scodoc import codes_cursus
    from app.scodoc import sco_groups
    from app.scodoc import sco_preferences
    from app.scodoc.codes_cursus import UE_SPORT, DEF
    from app.scodoc.sco_utils import fmt_note
    
    
    class BulletinBUT:
        """Génération du bulletin BUT.
        Cette classe génère des dictionnaires avec toutes les informations
        du bulletin, qui sont immédiatement traduisibles en JSON.
        """
    
        def __init__(self, formsemestre: FormSemestre):
            """ """
            self.res = ResultatsSemestreBUT(formsemestre)
            self.prefs = sco_preferences.SemPreferences(formsemestre.id)
    
        def etud_ue_mod_results(self, etud, ue, modimpls) -> dict:
            "dict synthèse résultats dans l'UE pour les modules indiqués"
            res = self.res
            d = {}
            etud_idx = res.etud_index[etud.id]
            if ue.type != UE_SPORT:
                ue_idx = res.modimpl_coefs_df.index.get_loc(ue.id)
            etud_moy_module = res.sem_cube[etud_idx]  # module x UE
            for modimpl in modimpls:
                if res.modimpl_inscr_df[modimpl.id][etud.id]:  # si inscrit
                    if ue.type != UE_SPORT:
                        coef = res.modimpl_coefs_df[modimpl.id][ue.id]
                        if coef > 0:
                            d[modimpl.module.code] = {
                                "id": modimpl.id,
                                "coef": coef,
                                "moyenne": fmt_note(
                                    etud_moy_module[
                                        res.modimpl_coefs_df.columns.get_loc(modimpl.id)
                                    ][ue_idx]
                                ),
                            }
                    # else:  # modules dans UE bonus sport
                    #     d[modimpl.module.code] = {
                    #         "id": modimpl.id,
                    #         "coef": "",
                    #         "moyenne": "?x?",
                    #     }
            return d
    
        def etud_ue_results(
            self,
            etud: Identite,
            ue: UniteEns,
            decision_ue: dict,
            etud_groups: list[GroupDescr] = None,
        ) -> dict:
            """dict synthèse résultats UE
            etud_groups : liste des groupes, pour affichage du rang.
            Si UE sport et étudiant non inscrit, renvoie dict vide.
            """
            res = self.res
    
            if (etud.id, ue.id) in self.res.dispense_ues:
                return {}
    
            if ue.type == UE_SPORT:
                modimpls_spo = [
                    modimpl
                    for modimpl in res.formsemestre.modimpls_sorted
                    if modimpl.module.ue.type == UE_SPORT
                ]
                # L'étudiant est-il inscrit à l'un des modules de l'UE bonus ?
                if not any(res.modimpl_inscr_df.loc[etud.id][[m.id for m in modimpls_spo]]):
                    return {}
    
            d = {
                "id": ue.id,
                "titre": ue.titre,
                "numero": ue.numero,
                "type": ue.type,
                "color": ue.color,
                "competence": None,  # XXX TODO lien avec référentiel
                "moyenne": None,
                # Le bonus sport appliqué sur cette UE
                "bonus": fmt_note(res.bonus_ues[ue.id][etud.id])
                if res.bonus_ues is not None and ue.id in res.bonus_ues
                else fmt_note(0.0),
                "malus": fmt_note(res.malus[ue.id][etud.id]),
                "capitalise": None,  # "AAAA-MM-JJ" TODO #sco93
                "ressources": self.etud_ue_mod_results(etud, ue, res.ressources),
                "saes": self.etud_ue_mod_results(etud, ue, res.saes),
            }
            if self.prefs["bul_show_ects"]:
                d["ECTS"] = {
                    "acquis": decision_ue.get("ects", 0.0),
                    "total": ue.ects or 0.0,  # float même si non renseigné
                }
            if ue.type != UE_SPORT:
                if self.prefs["bul_show_ue_rangs"]:
                    rangs, effectif = res.ue_rangs[ue.id]
                    rang = rangs[etud.id]
                else:
                    rang, effectif = "", 0
                d["moyenne"] = {
                    "value": fmt_note(res.etud_moy_ue[ue.id][etud.id]),
                    "min": fmt_note(res.etud_moy_ue[ue.id].min()),
                    "max": fmt_note(res.etud_moy_ue[ue.id].max()),
                    "moy": fmt_note(res.etud_moy_ue[ue.id].mean()),
                    "rang": rang,
                    "total": effectif,  # nb etud avec note dans cette UE
                    "groupes": {},
                }
                if self.prefs["bul_show_ue_rangs"]:
                    for group in etud_groups:
                        if group.partition.bul_show_rank:
                            rang, effectif = self.res.get_etud_ue_rang(
                                ue.id, etud.id, group.id
                            )
                            d["moyenne"]["groupes"][group.id] = {
                                "value": rang,
                                "total": effectif,
                            }
            else:  # UE BONUS
                d["modules"] = self.etud_mods_results(etud, modimpls_spo)
                # ceci suppose que l'on a une seule UE bonus,
                # en tous cas elles auront la même description
                d["bonus_description"] = self.etud_bonus_description(etud.id)
    
            return d
    
        def etud_ues_capitalisees(self, etud: Identite) -> dict:
            """dict avec les UE capitalisees. la clé est l'acronyme d'UE, qui ne
            peut donc être capitalisée qu'une seule fois (on prend la meilleure)"""
            if not etud.id in self.res.validations.ue_capitalisees.index:
                return {}  # aucune capitalisation
            d = {}
            for _, ue_capitalisee in self.res.validations.ue_capitalisees.loc[
                [etud.id]
            ].iterrows():
                if codes_cursus.code_ue_validant(ue_capitalisee.code):
                    ue = db.session.get(UniteEns, ue_capitalisee.ue_id)  # XXX cacher ?
                    # déjà capitalisé ? montre la meilleure
                    if ue.acronyme in d:
                        moy_cap = d[ue.acronyme]["moyenne_num"] or 0.0
                        if (not isinstance(moy_cap, float)) or (
                            (ue_capitalisee.moy_ue or 0.0) < moy_cap
                        ):
                            continue  # skip this duplicate UE
    
                    d[ue.acronyme] = {
                        "id": ue.id,
                        "ue_code": ue_capitalisee.ue_code,
                        "titre": ue.titre,
                        "numero": ue.numero,
                        "type": ue.type,
                        "color": ue.color,
                        "moyenne": fmt_note(ue_capitalisee.moy_ue),  # arrondi en str
                        "moyenne_num": fmt_note(ue_capitalisee.moy_ue, keep_numeric=True),
                        "is_external": ue_capitalisee.is_external,
                        "date_capitalisation": ue_capitalisee.event_date,
                        "formsemestre_id": ue_capitalisee.formsemestre_id,
                        "bul_orig_url": url_for(
                            "notes.formsemestre_bulletinetud",
                            scodoc_dept=g.scodoc_dept,
                            etudid=etud.id,
                            formsemestre_id=ue_capitalisee.formsemestre_id,
                        )
                        if ue_capitalisee.formsemestre_id
                        else None,
                        "ressources": {},  # sans détail en BUT
                        "saes": {},
                    }
                    if self.prefs["bul_show_ects"]:
                        d[ue.acronyme]["ECTS"] = {
                            "acquis": ue.ects or 0.0,  # toujours validée ici
                            "total": ue.ects or 0.0,  # float même si non renseigné
                        }
            return d
    
        def etud_mods_results(self, etud, modimpls, version="long") -> dict:
            """dict synthèse résultats des modules indiqués,
            avec évaluations de chacun (sauf si version == "short")
            """
            res = self.res
            d = {}
            # etud_idx = self.etud_index[etud.id]
            for modimpl in modimpls:
                # mod_idx = self.modimpl_coefs_df.columns.get_loc(mi.id)
                # # moyennes indicatives (moyennes de moyennes d'UE)
                # try:
                #     moyennes_etuds = np.nan_to_num(
                #         np.nanmean(self.sem_cube[:, mod_idx, :], axis=1),
                #         copy=False,
                #     )
                # except RuntimeWarning:
                # # all nans in np.nanmean (sur certains etuds sans notes valides)
                #     pass
                # try:
                #     moy_indicative_mod = np.nanmean(self.sem_cube[etud_idx, mod_idx])
                # except RuntimeWarning:  # all nans in np.nanmean
                #     pass
                modimpl_results = res.modimpls_results[modimpl.id]
                if res.modimpl_inscr_df[modimpl.id][etud.id]:  # si inscrit
                    d[modimpl.module.code] = {
                        "id": modimpl.id,
                        "titre": modimpl.module.titre,
                        "code_apogee": modimpl.module.code_apogee,
                        "url": url_for(
                            "notes.moduleimpl_status",
                            scodoc_dept=g.scodoc_dept,
                            moduleimpl_id=modimpl.id,
                        )
                        if has_request_context()
                        else "na",
                        "moyenne": {
                            # # moyenne indicative de module: moyenne des UE,
                            # # ignorant celles sans notes (nan)
                            # "value": fmt_note(moy_indicative_mod),
                            # "min": fmt_note(moyennes_etuds.min()),
                            # "max": fmt_note(moyennes_etuds.max()),
                            # "moy": fmt_note(moyennes_etuds.mean()),
                        },
                        "evaluations": [
                            self.etud_eval_results(etud, e)
                            for e in modimpl.evaluations
                            if (e.visibulletin or version == "long")
                            and (e.id in modimpl_results.evaluations_etat)
                            and (
                                modimpl_results.evaluations_etat[e.id].is_complete
                                or self.prefs["bul_show_all_evals"]
                            )
                        ]
                        if version != "short"
                        else [],
                    }
            return d
    
        def etud_eval_results(self, etud, e: Evaluation) -> dict:
            "dict resultats d'un étudiant à une évaluation"
            # eval_notes est une pd.Series avec toutes les notes des étudiants inscrits
            eval_notes = self.res.modimpls_results[e.moduleimpl_id].evals_notes[e.id]
            notes_ok = eval_notes.where(eval_notes > scu.NOTES_ABSENCE).dropna()
            modimpls_evals_poids = self.res.modimpls_evals_poids[e.moduleimpl_id]
            try:
                etud_ues_ids = self.res.etud_ues_ids(etud.id)
                poids = {
                    ue.acronyme: modimpls_evals_poids[ue.id][e.id]
                    for ue in self.res.ues
                    if (ue.type != UE_SPORT) and (ue.id in etud_ues_ids)
                }
            except KeyError:
                poids = collections.defaultdict(lambda: 0.0)
            d = {
                "id": e.id,
                "coef": fmt_note(e.coefficient)
                if e.evaluation_type == scu.EVALUATION_NORMALE
                else None,
                "date": e.jour.isoformat() if e.jour else None,
                "description": e.description,
                "evaluation_type": e.evaluation_type,
                "heure_debut": e.heure_debut.strftime("%H:%M") if e.heure_debut else None,
                "heure_fin": e.heure_fin.strftime("%H:%M") if e.heure_debut else None,
                "note": {
                    "value": fmt_note(
                        eval_notes[etud.id],
                        note_max=e.note_max,
                    ),
                    "min": fmt_note(notes_ok.min(), note_max=e.note_max),
                    "max": fmt_note(notes_ok.max(), note_max=e.note_max),
                    "moy": fmt_note(notes_ok.mean(), note_max=e.note_max),
                },
                "poids": poids,
                "url": url_for(
                    "notes.evaluation_listenotes",
                    scodoc_dept=g.scodoc_dept,
                    evaluation_id=e.id,
                )
                if has_request_context()
                else "na",
            }
            return d
    
        def etud_bonus_description(self, etudid):
            """description du bonus affichée dans la section "UE bonus"."""
            res = self.res
            if res.bonus_ues is None or res.bonus_ues.shape[1] == 0:
                return ""
    
            bonus_vect = res.bonus_ues.loc[etudid]
            if bonus_vect.nunique() > 1:
                # détail UE par UE
                details = [
                    f"{fmt_note(bonus_vect[ue.id])} sur {ue.acronyme}"
                    for ue in res.ues
                    if ue.type != UE_SPORT
                    and res.modimpls_in_ue(ue, etudid)
                    and ue.id in res.bonus_ues
                    and bonus_vect[ue.id] > 0.0
                ]
                if details:
                    return "Bonus de " + ", ".join(details)
                else:
                    return ""  # aucun bonus
            else:
                return f"Bonus de {fmt_note(bonus_vect.iloc[0])}"
    
        def bulletin_etud(
            self,
            etud: Identite,
            formsemestre: FormSemestre,
            force_publishing=False,
            version="long",
        ) -> dict:
            """Le bulletin de l'étudiant dans ce semestre: dict pour la version JSON / HTML.
            - version:
                "long", "selectedevals": toutes les infos (notes des évaluations)
                "short" : ne descend pas plus bas que les modules.
    
            - Si force_publishing, rempli le bulletin même si bul_hide_xml est vrai
            (bulletins non publiés).
            """
            res = self.res
            etat_inscription = etud.inscription_etat(formsemestre.id)
            nb_inscrits = self.res.get_inscriptions_counts()[scu.INSCRIT]
            published = (not formsemestre.bul_hide_xml) or force_publishing
            if formsemestre.formation.referentiel_competence is None:
                etud_ues_ids = {ue.id for ue in res.ues if res.modimpls_in_ue(ue, etud.id)}
            else:
                etud_ues_ids = res.etud_ues_ids(etud.id)
    
            d = {
                "version": "0",
                "type": "BUT",
                "date": datetime.datetime.utcnow().isoformat() + "Z",
                "publie": not formsemestre.bul_hide_xml,
                "etudiant": etud.to_dict_bul(),
                "formation": {
                    "id": formsemestre.formation.id,
                    "acronyme": formsemestre.formation.acronyme,
                    "titre_officiel": formsemestre.formation.titre_officiel,
                    "titre": formsemestre.formation.titre,
                },
                "formsemestre_id": formsemestre.id,
                "etat_inscription": etat_inscription,
                "options": sco_preferences.bulletin_option_affichage(
                    formsemestre, self.prefs
                ),
            }
            if not published:
                return d
    
            nbabs, nbabsjust = formsemestre.get_abs_count(etud.id)
            etud_groups = sco_groups.get_etud_formsemestre_groups(
                etud, formsemestre, only_to_show=True
            )
            semestre_infos = {
                "etapes": [str(x.etape_apo) for x in formsemestre.etapes if x.etape_apo],
                "date_debut": formsemestre.date_debut.isoformat(),
                "date_fin": formsemestre.date_fin.isoformat(),
                "annee_universitaire": formsemestre.annee_scolaire_str(),
                "numero": formsemestre.semestre_id,
                "inscription": "",  # inutilisé mais nécessaire pour le js de Seb.
                "groupes": [group.to_dict() for group in etud_groups],
            }
            if self.prefs["bul_show_abs"]:
                semestre_infos["absences"] = {
                    "injustifie": nbabs - nbabsjust,
                    "total": nbabs,
                }
            decisions_ues = self.res.get_etud_decisions_ue(etud.id) or {}
            if self.prefs["bul_show_ects"]:
                ects_tot = res.etud_ects_tot_sem(etud.id)
                ects_acquis = res.get_etud_ects_valides(etud.id, decisions_ues)
                semestre_infos["ECTS"] = {"acquis": ects_acquis, "total": ects_tot}
            if sco_preferences.get_preference("bul_show_decision", formsemestre.id):
                semestre_infos.update(
                    sco_bulletins_json.dict_decision_jury(etud, formsemestre)
                )
            if etat_inscription == scu.INSCRIT:
                # moyenne des moyennes générales du semestre
                semestre_infos["notes"] = {
                    "value": fmt_note(res.etud_moy_gen[etud.id]),
                    "min": fmt_note(res.etud_moy_gen.min()),
                    "moy": fmt_note(res.etud_moy_gen.mean()),
                    "max": fmt_note(res.etud_moy_gen.max()),
                }
                if self.prefs["bul_show_rangs"] and not np.isnan(res.etud_moy_gen[etud.id]):
                    # classement wrt moyenne générale, indicatif
                    semestre_infos["rang"] = {
                        "value": res.etud_moy_gen_ranks[etud.id],
                        "total": nb_inscrits,
                        "groupes": {},
                    }
                    # Rangs par groupes
                    for group in etud_groups:
                        if group.partition.bul_show_rank:
                            rang, effectif = self.res.get_etud_rang_group(etud.id, group.id)
                            semestre_infos["rang"]["groupes"][group.id] = {
                                "value": rang,
                                "total": effectif,
                            }
                else:
                    semestre_infos["rang"] = {
                        "value": "-",
                        "total": nb_inscrits,
                        "groupes": {},
                    }
                d.update(
                    {
                        "ressources": self.etud_mods_results(
                            etud, res.ressources, version=version
                        ),
                        "saes": self.etud_mods_results(etud, res.saes, version=version),
                        "ues_capitalisees": self.etud_ues_capitalisees(etud),
                        "semestre": semestre_infos,
                    },
                )
                d_ues = {}
                for ue in res.ues:
                    # si l'UE comporte des modules auxquels on est inscrit:
                    if (ue.type == UE_SPORT) or ue.id in etud_ues_ids:
                        ue_r = self.etud_ue_results(
                            etud,
                            ue,
                            decision_ue=decisions_ues.get(ue.id, {}),
                            etud_groups=etud_groups,
                        )
                        if ue_r:  # exclu UE sport sans inscriptions
                            d_ues[ue.acronyme] = ue_r
                d["ues"] = d_ues
    
            else:
                semestre_infos.update(
                    {
                        "notes": {
                            "value": "DEM",
                            "min": "",
                            "moy": "",
                            "max": "",
                        },
                        "rang": {"value": "DEM", "total": nb_inscrits},
                    }
                )
                d.update(
                    {
                        "semestre": semestre_infos,
                        "ressources": {},
                        "saes": {},
                        "ues": {},
                        "ues_capitalisees": {},
                    }
                )
    
            return d
    
        def bulletin_etud_complet(self, etud: Identite, version="long") -> dict:
            """Bulletin dict complet avec toutes les infos pour les bulletins BUT pdf
            (pas utilisé pour json/html)
            Résultat compatible avec celui de sco_bulletins.formsemestre_bulletinetud_dict
            """
            d = self.bulletin_etud(
                etud, self.res.formsemestre, version=version, force_publishing=True
            )
            d["etudid"] = etud.id
            d["etud"] = d["etudiant"]
            d["etud"]["nomprenom"] = etud.nomprenom
            d["etud"]["etat_civil"] = etud.etat_civil
            d.update(self.res.sem)
            etud_etat = self.res.get_etud_etat(etud.id)
            d["filigranne"] = sco_bulletins_pdf.get_filigranne(
                etud_etat,
                self.prefs,
                decision_sem=d["semestre"].get("decision"),
            )
            if etud_etat == scu.DEMISSION:
                d["demission"] = "(Démission)"
            elif etud_etat == DEF:
                d["demission"] = "(Défaillant)"
            else:
                d["demission"] = ""
    
            # --- Absences
            d["nbabs"], d["nbabsjust"] = self.res.formsemestre.get_abs_count(etud.id)
    
            # --- Decision Jury
            infos, dpv = sco_bulletins.etud_descr_situation_semestre(
                etud.id,
                self.res.formsemestre,
                format="html",
                show_date_inscr=self.prefs["bul_show_date_inscr"],
                show_decisions=self.prefs["bul_show_decision"],
                show_uevalid=self.prefs["bul_show_uevalid"],
                show_mention=self.prefs["bul_show_mention"],
            )
    
            d.update(infos)
            # --- Rangs
            d[
                "rang_nt"
            ] = f"{d['semestre']['rang']['value']} / {d['semestre']['rang']['total']}"
            d["rang_txt"] = "Rang " + d["rang_nt"]
    
            # --- Appréciations
            d.update(
                sco_bulletins.get_appreciations_list(self.res.formsemestre.id, etud.id)
            )
            d.update(sco_bulletins.make_context_dict(self.res.formsemestre, d["etud"]))
    
            return d