ScoDoc/app/tables/jury_recap.py

353 lines
14 KiB
Python

##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Jury BUT et classiques: table recap annuelle et liens saisie
"""
import collections
import time
import numpy as np
from flask import g, url_for
from app import log
from app.but import cursus_but
from app.but import jury_but
from app.but.jury_but import DecisionsProposeesRCUE
from app.comp.res_compat import NotesTableCompat
from app.models import ApcNiveau, UniteEns, ValidationDUT120
from app.models.etudiants import Identite
from app.scodoc.codes_cursus import (
BUT_BARRE_RCUE,
BUT_RCUE_SUFFISANT,
)
from app.scodoc import sco_utils as scu
from app.tables.recap import RowRecap, TableRecap
class TableJury(TableRecap):
"""Cette table recap reprend les colonnes du tableau recap, sauf les évaluations,
et ajoute:
Pour le BUT:
- les RCUEs (moyenne et code décision)
Pour toutes les formations:
- les codes de décisions jury sur les UEs
- les ECTS acquis dans le parcours
- le lien de saisie ou modif de la décision de jury
"""
def __init__(self, *args, row_class: str = None, read_only=True, **kwargs):
t0 = time.time()
super().__init__(
*args, row_class=row_class or RowJury, finalize=False, **kwargs
)
# redéclare pour VSCode
self.rows: list["RowJury"] = self.rows
self.res: NotesTableCompat = self.res
self.read_only = read_only
# Stats jury: fréquence de chaque code enregistré
self.freq_codes_annuels = collections.Counter()
# Ajout colonnes spécifiques à la table jury:
if self.rows: # non vide
if self.res.is_apc:
self.add_but_competences()
self.add_rcues()
self.add_jury()
self.add_groups_header()
# Termine la table
self.finalize()
log(f"Built TableJury for {self.res.formsemestre} in {time.time() - t0}s")
def add_rcues(self):
"""Ajoute les colonnes indiquant le nb de RCUEs et chaque RCUE
pour tous les étudiants de la table.
La table contient des rows avec la clé etudid.
Les colonnes ont la classe css "rcue".
"""
self.insert_group("rcue", before="col_ues_validables")
for row in self.rows:
deca = row.deca
if deca.code_valide:
self.freq_codes_annuels[deca.code_valide] += 1
row.add_nb_rcues_cell()
# --- Les RCUEs
for dec_rcue in deca.get_decisions_rcues_annee():
if dec_rcue.rcue.complete:
row.add_rcue_cols(dec_rcue)
self.freq_codes_annuels["total"] = len(self.rows)
def add_jury(self):
"""Ajoute la colonne code jury, ECTS et le lien.
- Le code jury est celui du semestre: cette colonne n'est montrée
que pour les formations classiques, ce code n'est pas utilisé en BUT.
- En BUT, on donne la décision de jury annuelle.
"""
res = self.res
autorisations = res.get_autorisations_inscription()
if res.is_apc:
validations_annee = res.get_validations_annee()
for row in self.rows:
etud = row.etud
if not res.is_apc:
# formations classiques: code semestre
if res.validations:
dec_sem = res.validations.decisions_jury.get(etud.id)
jury_code_sem = dec_sem["code"] if dec_sem else ""
else:
jury_code_sem = ""
row.add_cell(
"jury_code_sem",
"Jury",
jury_code_sem or "",
group="jury_code_sem",
classes=[] if jury_code_sem else ["empty_code"],
)
self.foot_title_row.cells["jury_code_sem"].target_attrs[
"title"
] = """Code jury sur le semestre"""
# Autorisations inscription ou diplôme BUT S6
if res.is_apc and res.formsemestre.semestre_id == 6:
# on ne vérifie le diplôme que dans ce cas pour ne pas ralentir
if cursus_but.but_parcours_validated(
etud.id, res.etuds_parcour_id.get(etud.id)
):
row.add_cell(
"autorisations_inscription",
"Passage",
"Diplôme obtenu",
group="jury_code_sem",
classes=["recorded_code"],
)
else:
row.add_cell(
"autorisations_inscription",
"Passage",
(
", ".join("S" + str(i) for i in sorted(autorisations[etud.id]))
if etud.id in autorisations
else ""
),
group="jury_code_sem",
classes=["recorded_code"],
)
if res.is_apc: # BUT
validation_annee = validations_annee.get(etud.id, None)
row.add_cell(
"decision_annuelle",
"Année",
validation_annee.code if validation_annee else "",
group="jury_code_sem",
classes=["recorded_code"],
)
# ECTS acquis en BUT
if res.formsemestre.formation.referentiel_competence_id:
row.add_cell(
"ects_acquis",
"ECTS",
# res.get_etud_ects_valides(etud.id),
# cette recherche augmente de 10% le temps de construction de la table
cursus_but.but_ects_valides(
etud, res.formsemestre.formation.referentiel_competence_id
),
group="jury_code_sem",
classes=["recorded_code"],
target_attrs={"title": "crédits validés en BUT"},
)
# Diplôme DUT120
validation_dut120 = ValidationDUT120.query.filter_by(etudid=etud.id).first()
if validation_dut120:
row.add_cell(
"dut120",
"DUT",
"DUT",
group="jury_code_sem",
classes=["recorded_code"],
)
# Lien saisie ou visu jury
a_saisir = (not res.validations) or (not res.validations.has_decision(etud))
row.add_cell(
"jury_link",
"",
f"""{("➨ saisir" if a_saisir else "modifier")
if not self.read_only else "voir"} décisions""",
group="col_jury_link",
classes=["fontred"] if a_saisir else [],
no_excel=True,
target=url_for(
"notes.formsemestre_validation_etud_form",
scodoc_dept=g.scodoc_dept,
formsemestre_id=res.formsemestre.id,
etudid=etud.id,
),
target_attrs={"class": "stdlink"},
)
def add_but_competences(self):
"Ajoute les colonnes résultats BUT (niveaux de compétences des 3 années)"
prev_group = "cursus"
for annee in ("BUT1", "BUT2", "BUT3"):
group = f"cursus_{annee}"
self.insert_group(group, after=prev_group)
prev_group = group
self.group_titles[group] = f"Compétences {annee}"
for row in self.rows:
etud = row.etud
cursus_dict = cursus_but.EtudCursusBUT(
etud, self.res.formsemestre.formation
).to_dict()
first = True
for competence_id in cursus_dict:
for annee in ("BUT1", "BUT2", "BUT3"):
validation_rcue = cursus_dict[competence_id][annee]
if validation_rcue:
niveau: ApcNiveau = validation_rcue.niveau()
titre = f"C{niveau.competence.numero}" # à voir (nommer les compétences...)
row.add_cell(
f"c_{competence_id}_{annee}",
titre,
validation_rcue.code,
group="cursus_" + annee,
classes=[],
column_classes=["cursus_but" + (" first" if first else "")],
target_attrs={
"title": f"{niveau.competence.titre} niveau {niveau.ordre}"
},
)
first = False
class RowJury(RowRecap):
"Ligne de la table saisie jury"
def __init__(self, table: TableJury, etud: Identite, *args, **kwargs):
self.table: TableJury = table
super().__init__(table, etud, *args, **kwargs)
if table.res.is_apc:
# Conserve le deca de cet étudiant:
self.deca = jury_but.DecisionsProposeesAnnee(
self.etud, self.table.res.formsemestre
)
def add_nb_rcues_cell(self):
"cell avec nb niveaux validables / total"
if self.table.res.get_etud_etat(self.id) != scu.INSCRIT:
return # rien pour DEM et DEF
deca = self.deca
classes = ["col_rcue", "col_rcues_validables"]
if deca.nb_rcues_under_8 > 0:
classes.append("moy_ue_warning")
elif deca.nb_validables < deca.nb_competences:
classes.append("moy_ue_inf")
else:
classes.append("moy_ue_valid")
if len(deca.get_decisions_rcues_annee()) > 0:
# permet un tri par nb de niveaux validables + moyenne gen indicative S_pair
if deca.res_pair and deca.etud.id in deca.res_pair.etud_moy_gen:
moy = deca.res_pair.etud_moy_gen[deca.etud.id]
if np.isnan(moy):
moy_gen_d = "x"
else:
moy_gen_d = f"{int(moy*1000):05}"
else:
moy_gen_d = "x"
order = f"{deca.nb_validables:04d}-{moy_gen_d}"
else:
# étudiants sans RCUE: pas de semestre impair, ...
# les classe à la fin
order = f"{deca.nb_validables:04d}-00000-{deca.etud.sort_key}"
# RCUE pouvant être validés
self.add_cell(
"rcues_validables",
"RCUEs",
f"""{deca.nb_validables}/{deca.nb_competences}"""
+ ((" " + scu.EMO_WARNING) if deca.nb_rcues_under_8 > 0 else ""),
raw_content=f"""{deca.nb_validables}/{deca.nb_competences}""",
group="rcue",
classes=classes,
data={"order": order},
)
def add_ue_cols(self, ue: UniteEns, ue_status: dict, col_group: str = None):
"Ajoute 2 colonnes: moyenne d'UE et code jury"
# table recap standard (mais avec group différent)
super().add_ue_cols(ue, ue_status, col_group=col_group or "col_ue")
dues = self.table.res.get_etud_decisions_ue(self.etud.id)
due = dues.get(ue.id) if dues else None
col_id = f"moy_ue_{ue.id}_code"
title = (
f"""{ue.acronyme} enregistrée le {due['event_date']}, {
(due["ects"] or 0):.3g}&nbsp;ECTS."""
if due
else """pas de décision"""
)
self.add_cell(
col_id,
"", # titre vide
due["code"] if due else "",
raw_content=due["code"] if due else "",
group="col_ue",
classes=["recorded_code"],
column_classes={"col_jury", "col_ue_code"},
target_attrs={"title": title},
)
def add_rcue_cols(self, dec_rcue: DecisionsProposeesRCUE):
"2 cells: moyenne du RCUE, code enregistré"
rcue = dec_rcue.rcue
if not rcue.complete:
return
col_id = f"moy_rcue_{rcue.niveau.id}" # le niveau_id
self.table.group_titles["rcue"] = "RCUEs en cours"
note_class = ""
val = rcue.moy_rcue
if isinstance(val, float):
if val < BUT_BARRE_RCUE:
note_class = "moy_ue_inf"
elif val >= BUT_BARRE_RCUE:
note_class = "moy_ue_valid"
if val < BUT_RCUE_SUFFISANT:
note_class = "moy_ue_warning" # notes très basses
self.add_cell(
col_id,
f"<div>{rcue.ue_1.acronyme}</div><div>{rcue.ue_2.acronyme}</div>",
self.table.fmt_note(val),
raw_content=val,
raw_title=f"{rcue.ue_1.acronyme}-{rcue.ue_2.acronyme}",
group="rcue",
classes=[note_class],
column_classes={"col_rcue"},
)
self.add_cell(
col_id + "_code",
f"<div>{rcue.ue_1.acronyme}</div><div>{rcue.ue_2.acronyme}</div>",
dec_rcue.code_valide or "",
group="rcue",
classes=[
"col_rcue_code",
"recorded_code",
"empty_code" if not dec_rcue.code_valide else "",
],
column_classes={"col_rcue"},
raw_title=f"{rcue.ue_1.acronyme}-{rcue.ue_2.acronyme}",
)
# # --- Les ECTS validés
# ects_valides = 0.0
# if deca.res_impair:
# ects_valides += deca.res_impair.get_etud_ects_valides(etudid)
# if deca.res_pair:
# ects_valides += deca.res_pair.get_etud_ects_valides(etudid)
# row.add_cell(
# "ects_annee",
# "ECTS",
# f"""{int(ects_valides)}""",
# "col_code_annee",
# )