# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Emmanuel Viennet emmanuel.viennet@viennet.net # ############################################################################## """Rapport sur réussite en BUT pour enquête 2022 - statistiques decisions """ from collections import defaultdict from flask import request from app import db from app.but import jury_but from app.models import FormSemestre from app.models.formsemestre import FormSemestreInscription import app.scodoc.sco_utils as scu from app.scodoc import html_sco_header from app.scodoc import codes_cursus from app.scodoc.sco_exceptions import ScoValueError from app.scodoc import sco_preferences import sco_version from app.scodoc.gen_tables import GenTable # Titres, ordonnés INDICATEUR_NAMES = { "nb_inscr_S1": "Inscrits initiaux S1", "nb_dem_S1": "Démissions S1", "nb_def_S1": "Défaillants S1", "nb_actifs_S1": "Inscrits finals S1", "nb_inscr_S2": "Inscrits initiaux S2", "nb_dem_S2": "Démissions S2", "nb_def_S2": "Défaillants S2", "nb_actifs_S2": "Inscrits finals S2", "nb_no_decision": "Sans décision de jury BUT", "nb_nar": "NAR", "nb_passe_manque_rcue": "Passant avec RCUE non validé", "nb_red_avec_rcue": "Redoublant avec au moins un RCUE validé", "nb_red_sans_rcue": "Redoublant sans avoir validé aucun RCUE", "nb_valide_tt_rcue": "Validant tous les RCUE de l'année", } def formsemestre_but_indicateurs(formsemestre_id: int, fmt="html"): """Page avec tableau indicateurs enquête ADIUT BUT 2022""" formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) indicateurs_by_bac = but_indicateurs_by_bac(formsemestre) # finalement on fait une table avec en ligne # les indicateurs, et en colonne les bacs bacs = sorted(indicateurs_by_bac.keys()) rows = [] for indicateur, titre_indicateur in INDICATEUR_NAMES.items(): row = {bac: indicateurs_by_bac[bac][indicateur] for bac in bacs} row["titre_indicateur"] = titre_indicateur rows.append(row) # ensure Total is the rightmost column del bacs[bacs.index("Total")] bacs.append("Total") tab = GenTable( titles={bac: bac for bac in bacs}, columns_ids=["titre_indicateur"] + bacs, rows=rows, html_sortable=False, preferences=sco_preferences.SemPreferences(formsemestre_id), filename=scu.make_filename(f"Indicateurs_BUT_{formsemestre.titre_annee()}"), origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}", html_caption="Indicateurs BUT annuels.", base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}", ) title = "Indicateurs suivi annuel BUT" t = tab.make_page( title=f"""<h2 class="formsemestre">{title}</h2>""", fmt=fmt, with_html_headers=False, ) if fmt != "html": return t H = [ html_sco_header.sco_header(page_title=title), t, """<p class="help"> </p>""", html_sco_header.sco_footer(), ] return "\n".join(H) def but_indicateurs_by_bac(formsemestre: FormSemestre) -> dict[str:dict]: """ L'enquête ADIUT porte sur le nombre de - inscrits - ayant validé tous les RCUE - passant en BUT2 sans avoir validé tous les RCUE - redoublants avec au moins une RCUE - redoublants sans aucune RCUE - NAR - défaillants - démissionnaires, le tout découpé en FI/FA et suivant le type de bac : général/techno/pro/autre. Le semestre est FI ou FA, donc on ne traite pas ce point. On suppose qu'on est sur un semestre PAIR de BUT, dont les décisions de jury ont déjà été saisies. """ if not formsemestre.formation.is_apc(): raise ScoValueError( "Ce rapport doit être généré à partir d'une formation par compétences (BUT)." ) if formsemestre.semestre_id % 2: raise ScoValueError("Ce rapport doit être généré à partir d'un semestre PAIR.") # Les semestres suivant/precedent (pour compter les passages et redoublements) next_sem_idx = formsemestre.semestre_id + 1 red_sem_idx = formsemestre.semestre_id - 1 # Ventilation par bac inscriptions_by_bac = _formsemestre_inscriptions_by_bac(formsemestre) indicateurs_by_bac = {} decisions_annee_tous = {} for bac in inscriptions_by_bac: decisions_annee = { inscr.etud.id: jury_but.DecisionsProposeesAnnee(inscr.etud, formsemestre) for inscr in inscriptions_by_bac[bac] } indicateurs_by_bac[bac] = _indicateurs_enquete_but( inscriptions_by_bac[bac], decisions_annee, red_sem_idx, next_sem_idx ) decisions_annee_tous.update(decisions_annee) # refait pour tous indicateurs_by_bac["Total"] = _indicateurs_enquete_but( formsemestre.inscriptions, decisions_annee_tous, red_sem_idx, next_sem_idx ) # Comptages sur semestre(s) précédent(s) # en effet, les étudiants de ce semestre pair peuvent venir de # différents S1 formsemestre_id_precedents = { deca.formsemestre_impair.id for deca in decisions_annee_tous.values() if deca and deca.formsemestre_impair } for formsemestre_id_precedent in formsemestre_id_precedents: formsemestre_impair = db.session.get(FormSemestre, formsemestre_id_precedent) suffix = ( f"S{formsemestre_impair.semestre_id}" if len(formsemestre_id_precedents) == 1 else formsemestre_impair.session_id() ) for bac, inscriptions in _formsemestre_inscriptions_by_bac( formsemestre_impair ).items(): if bac not in indicateurs_by_bac: indicateurs_by_bac[bac] = defaultdict(lambda: 0) indicateurs_by_bac[bac].update( _indicateurs_enquete_but_inscrits(inscriptions, suffix) ) indicateurs_by_bac["Total"].update( _indicateurs_enquete_but_inscrits(formsemestre_impair.inscriptions, suffix) ) return indicateurs_by_bac def _formsemestre_inscriptions_by_bac(formsemestre: FormSemestre) -> defaultdict: "liste d'inscriptions, par type de bac" inscriptions_by_bac = defaultdict(list) # bac : etuds for inscr in formsemestre.inscriptions: adm = inscr.etud.admission bac = adm.get_bac().abbrev() if adm else "?" inscriptions_by_bac[bac].append(inscr) return inscriptions_by_bac def _indicateurs_enquete_but_inscrits( inscriptions: list[FormSemestreInscription], suffix: str ) -> dict: """Nombre d'incrits, DEM, DEF. Suffixe les clés avec _suffix""" if suffix: suffix = "_" + suffix return defaultdict( lambda: "?", { "nb_inscr" + suffix: len(inscriptions), "nb_actifs" + suffix: len([i for i in inscriptions if i.etat == scu.INSCRIT]), "nb_def" + suffix: len([i for i in inscriptions if i.etat == scu.DEF]), "nb_dem" + suffix: len([i for i in inscriptions if i.etat == scu.DEMISSION]), }, ) def _indicateurs_enquete_but( inscriptions: list[FormSemestreInscription], decisions_annee: dict[jury_but.DecisionsProposeesAnnee], red_sem_idx: int, next_sem_idx: int, ) -> dict: """Calcule les indicateurs de l'enquête ADIUT 2022""" indicateurs = _indicateurs_enquete_but_inscrits(inscriptions, suffix="S2") indicateurs.update( { "nb_no_decision": len( [True for deca in decisions_annee.values() if deca.code_valide is None] ), "nb_nar": len( [ True for deca in decisions_annee.values() if deca.code_valide == codes_cursus.NAR ] ), # Redoublants sans aucune RCUE "nb_red_sans_rcue": len( [ True for deca in decisions_annee.values() if (deca.nb_rcue_valides == 0) and (next_sem_idx not in deca.get_autorisations_passage()) and (red_sem_idx in deca.get_autorisations_passage()) ] ), # Redoublants avec au moins une RCUE "nb_red_avec_rcue": len( [ True for deca in decisions_annee.values() if (deca.nb_rcue_valides > 0) and (next_sem_idx not in deca.get_autorisations_passage()) and (red_sem_idx in deca.get_autorisations_passage()) ] ), # Passant (en BUT2) sans avoir validé tous les RCUE "nb_passe_manque_rcue": len( [ True for deca in decisions_annee.values() if (deca.nb_rcue_valides < deca.nb_competences) and (next_sem_idx in deca.get_autorisations_passage()) ] ), # Ayant validé tous les RCUE "nb_valide_tt_rcue": len( [ True for deca in decisions_annee.values() if (deca.nb_rcue_valides >= deca.nb_competences) ] ), } ) return indicateurs