ScoDoc/app/scodoc/sco_report_but.py

277 lines
10 KiB
Python

# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2024 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 render_template, 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 codes_cursus
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_preferences
from app.scodoc.gen_tables import GenTable
import sco_version
# 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.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(
base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}",
columns_ids=["titre_indicateur"] + bacs,
filename=scu.make_filename(f"Indicateurs_BUT_{formsemestre.titre_annee()}"),
html_caption="Indicateurs BUT annuels.",
html_sortable=False,
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
preferences=sco_preferences.SemPreferences(formsemestre_id),
rows=rows,
titles={bac: bac for bac in bacs},
table_id="formsemestre_but_indicateurs",
)
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 = [
t,
"""<p class="help">
</p>""",
]
return render_template("sco_page.j2", title=title, content="\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