############################################################################## # ScoDoc # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## """Jury BUT: table recap annuelle et liens saisie """ import collections import time import numpy as np from flask import g, url_for from app.but import jury_but from app.but.jury_but import ( DecisionsProposeesAnnee, DecisionsProposeesRCUE, DecisionsProposeesUE, ) from app.comp.res_but import ResultatsSemestreBUT from app.comp import res_sem from app.models.etudiants import Identite from app.scodoc.sco_exceptions import ScoNoReferentielCompetences from app.models.formsemestre import FormSemestre from app.scodoc import html_sco_header from app.scodoc.sco_codes_parcours import ( BUT_BARRE_RCUE, BUT_BARRE_UE, BUT_BARRE_UE8, BUT_RCUE_SUFFISANT, ) from app.scodoc import sco_formsemestre_status from app.scodoc import sco_pvjury from app.scodoc import sco_utils as scu from app.scodoc import table_builder as tb class TableJury(tb.Table): pass class RowJury(tb.Row): "Ligne de la table saisie jury" def add_nb_rcues_cell(self, deca: DecisionsProposeesAnnee): "cell avec nb niveaux validables / total" 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.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}" self.add_cell( "rcues_validables", "RCUEs", f"""{deca.nb_validables}/{deca.nb_competences}""" + ((" " + scu.EMO_WARNING) if deca.nb_rcues_under_8 > 0 else ""), group="rcues_validables", classes=classes, data={"order": order}, ) def add_ue_cells(self, dec_ue: DecisionsProposeesUE): "cell de moyenne d'UE" col_id = f"moy_ue_{dec_ue.ue.id}" note_class = "" val = dec_ue.moy_ue if isinstance(val, float): if val < BUT_BARRE_UE: note_class = "moy_inf" elif val >= BUT_BARRE_UE: note_class = "moy_ue_valid" if val < BUT_BARRE_UE8: note_class = "moy_ue_warning" # notes très basses self.add_cell( col_id, dec_ue.ue.acronyme, self.fmt_note(val), group="col_ue", "col_ue" + note_class, column_class="col_ue", ) self.add_cell( col_id + "_code", dec_ue.ue.acronyme, dec_ue.code_valide or "", "col_ue_code recorded_code", column_class="col_ue", ) def formsemestre_saisie_jury_but( formsemestre2: FormSemestre, read_only: bool = False, selected_etudid: int = None, mode="jury", ) -> str: """formsemestre est un semestre PAIR Si readonly, ne montre pas le lien "saisir la décision" => page html complète Si mode == "recap", table recap des codes, sans liens de saisie. """ # Quick & Dirty # pour chaque etud de res2 trié # S1: UE1, ..., UEn # S2: UE1, ..., UEn # # UE1_s1, UE1_s2, moy_rcue, UE2... , Nbrcue_validables, Nbrcue<8, passage_de_droit, valide_moitie_rcue # # Pour chaque etud de res2 trié # DecisionsProposeesAnnee(etud, formsemestre2) # Pour le 1er etud, faire un check_ues_ready_jury(self) -> page d'erreur # -> rcue .ue_1, .ue_2 -> stroe moy ues, rcue.moy_rcue, etc if formsemestre2.formation.referentiel_competence is None: raise ScoNoReferentielCompetences(formation=formsemestre2.formation) rows, titles, column_ids, jury_stats = get_jury_but_table( formsemestre2, read_only=read_only, mode=mode ) if not rows: return ( '
aucun étudiant !
' ) filename = scu.sanitize_filename( f"""jury-but-{formsemestre2.titre_num()}-{time.strftime("%Y-%m-%d")}""" ) klass = "table_jury_but_bilan" if mode == "recap" else "" table_html = build_table_jury_but_html( filename, rows, titles, column_ids, selected_etudid=selected_etudid, klass=klass ) H = [ html_sco_header.sco_header( page_title=f"{formsemestre2.sem_modalite()}: jury BUT annuel", no_side_bar=True, init_qtip=True, javascripts=["js/etud_info.js", "js/table_recap.js"], ), sco_formsemestre_status.formsemestre_status_head( formsemestre_id=formsemestre2.id ), ] if mode == "recap": H.append( f"""

Décisions de jury enregistrées pour les étudiants de ce semestre

""" ) H.append( f""" {table_html}
Nb d'étudiants avec décision annuelle: {sum(jury_stats["codes_annuels"].values())} / {jury_stats["nb_etuds"]}
Codes annuels octroyés:
""" ) for code in sorted(jury_stats["codes_annuels"].keys()): H.append( f"""""" ) H.append( f"""
{code} {jury_stats["codes_annuels"][code]} { (100*jury_stats["codes_annuels"][code] / jury_stats["nb_etuds"]):2.1f}%
{html_sco_header.sco_footer()} """ ) return "\n".join(H) def build_table_jury_but_html( filename: str, rows, titles, column_ids, selected_etudid: int = None, klass="" ) -> str: """assemble la table html""" footer_rows = [] # inutilisé pour l'instant H = [ f"""
""" ] # header H.append( f""" {scu.gen_row(column_ids, titles, "th")} """ ) # body H.append("") for row in rows: H.append(f"{scu.gen_row(column_ids, row, selected_etudid=selected_etudid)}\n") H.append("\n") # footer H.append("") idx_last = len(footer_rows) - 1 for i, row in enumerate(footer_rows): H.append(f'{scu.gen_row(column_ids, row, "th" if i == idx_last else "td")}\n') H.append( """
""" ) return "".join(H) class RowCollector: """Une ligne de la table""" def __init__( self, cells: dict = None, titles: dict = None, convert_values=True, column_classes: dict = None, ): self.titles = titles self.row = cells or {} # col_id : str self.column_classes = column_classes # col_id : str, css class self.idx = 0 self.last_etud_cell_idx = 0 if convert_values: self.fmt_note = scu.fmt_note else: self.fmt_note = lambda x: x def __setitem__(self, key, value): self.row[key] = value def __getitem__(self, key): return self.row[key] def get_row_dict(self): "La ligne, comme un dict" # create empty cells for col_id in self.titles: if col_id not in self.row: self.row[col_id] = "" klass = self.column_classes.get(col_id) if klass: self.row[f"_{col_id}_class"] = klass return self.row def add_cell( self, col_id: str, title: str, content: str, classes: str = "", idx: int = None, column_class="", ): """Add a row to our table. classes is a list of css class names""" self.idx = idx if idx is not None else self.idx self.row[col_id] = content if classes: self.row[f"_{col_id}_class"] = classes + f" c{self.idx}" if not col_id in self.titles: self.titles[col_id] = title self.titles[f"_{col_id}_col_order"] = self.idx if classes: self.titles[f"_{col_id}_class"] = classes self.column_classes[col_id] = column_class self.idx += 1 # def add_etud_cells( # self, etud: Identite, formsemestre: FormSemestre, with_links=True # ): # "Les cells code, nom, prénom etc." # # --- Codes (seront cachés, mais exportés en excel) # self.add_cell("etudid", "etudid", etud.id, "codes") # self.add_cell("code_nip", "code_nip", etud.code_nip or "", "codes") # # --- Identité étudiant (adapté de res_common/get_table_recap, à factoriser XXX TODO) # self.add_cell("civilite_str", "Civ.", etud.civilite_str, "identite_detail") # self.add_cell("nom_disp", "Nom", etud.nom_disp(), "identite_detail") # self["_nom_disp_order"] = etud.sort_key # self.add_cell("prenom", "Prénom", etud.prenom, "identite_detail") # self.add_cell("nom_short", "Nom", etud.nom_short, "identite_court") # self["_nom_short_data"] = { # "etudid": etud.id, # "nomprenom": etud.nomprenom, # } # if with_links: # self["_nom_short_order"] = etud.sort_key # self["_nom_short_target"] = url_for( # "notes.formsemestre_bulletinetud", # scodoc_dept=g.scodoc_dept, # formsemestre_id=formsemestre.id, # etudid=etud.id, # ) # self["_nom_short_target_attrs"] = f'class="etudinfo" id="{etud.id}"' # self["_nom_disp_target"] = self["_nom_short_target"] # self["_nom_disp_target_attrs"] = self["_nom_short_target_attrs"] # self.last_etud_cell_idx = self.idx def add_rcue_cells(self, dec_rcue: DecisionsProposeesRCUE): "2 cells: moyenne du RCUE, code enregistré" rcue = dec_rcue.rcue col_id = f"moy_rcue_{rcue.ue_1.niveau_competence_id}" # le niveau_id 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"
{rcue.ue_1.acronyme}
{rcue.ue_2.acronyme}
", self.fmt_note(val), "col_rcue" + note_class, column_class="col_rcue", ) self.add_cell( col_id + "_code", f"
{rcue.ue_1.acronyme}
{rcue.ue_2.acronyme}
", dec_rcue.code_valide or "", "col_rcue_code recorded_code", column_class="col_rcue", ) def get_jury_but_table( formsemestre2: FormSemestre, read_only: bool = False, mode="jury", with_links=True ) -> tuple[list[dict], list[str], list[str], dict]: """Construit la table des résultats annuels pour le jury BUT => rows_dict, titles, column_ids, jury_stats où jury_stats est un dict donnant des comptages sur le jury. """ res2: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre2) titles = {} # column_id : title jury_stats = { "nb_etuds": len(formsemestre2.etuds_inscriptions), "codes_annuels": collections.Counter(), } table = TableJury(res2, mode_jury=True) for etudid in formsemestre2.etuds_inscriptions: etud: Identite = Identite.query.get(etudid) deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre2) # XXX row = RowCollector(titles=titles, column_classes=column_classes) row = RowJury(table, etudid) table.add_row(row) row.add_etud(etud) # --- Nombre de niveaux row.add_nb_rcues_cell(deca) # --- Les RCUEs for rcue in deca.rcues_annee: dec_rcue = deca.dec_rcue_by_ue.get(rcue.ue_1.id) if dec_rcue is not None: # None si l'UE n'est pas associée à un niveau row.add_ue_cells(deca.decisions_ues[rcue.ue_1.id]) row.add_ue_cells(deca.decisions_ues[rcue.ue_2.id]) row.add_rcue_cells(dec_rcue) # --- 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", ) # --- Le code annuel existant row.add_cell( "code_annee", "Année", f"""{deca.code_valide or ''}""", "col_code_annee", ) if deca.code_valide: jury_stats["codes_annuels"][deca.code_valide] += 1 # --- Le lien de saisie if mode != "recap" and with_links: row.add_cell( "lien_saisie", "", f""" {"voir" if read_only else ("modif." if deca.code_valide else "saisie")} décision """ if deca.inscription_etat == scu.INSCRIT else deca.inscription_etat, "col_lien_saisie_but", ) rows.append(row) rows_dict = [row.get_row_dict() for row in rows] if len(rows_dict) > 0: col_idx = res2.recap_add_partitions( rows_dict, titles, col_idx=row.last_etud_cell_idx + 1 ) res2.recap_add_cursus(rows_dict, titles, col_idx=col_idx + 1) column_ids = [title for title in titles if not title.startswith("_")] column_ids.sort(key=lambda col_id: titles.get("_" + col_id + "_col_order", 1000)) rows_dict.sort(key=lambda row: row["_nom_disp_order"]) return rows_dict, titles, column_ids, jury_stats def get_jury_but_results(formsemestre: FormSemestre) -> list[dict]: """Liste des résultats jury BUT sous forme de dict, pour API""" if formsemestre.formation.referentiel_competence is None: # pas de ref. comp., donc pas de decisions de jury (ne lance pas d'exception) return [] dpv = sco_pvjury.dict_pvjury(formsemestre.id) rows = [] for etudid in formsemestre.etuds_inscriptions: rows.append(get_jury_but_etud_result(formsemestre, dpv, etudid)) return rows def get_jury_but_etud_result( formsemestre: FormSemestre, dpv: dict, etudid: int ) -> dict: """Résultats de jury d'un étudiant sur un semestre pair de BUT""" etud: Identite = Identite.query.get(etudid) dec_etud = dpv["decisions_dict"][etudid] if formsemestre.formation.is_apc(): deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) else: deca = None row = { "etudid": etud.id, "code_nip": etud.code_nip, "code_ine": etud.code_ine, "is_apc": dpv["is_apc"], # BUT ou classic ? "etat": dec_etud["etat"], # I ou D ou DEF "nb_competences": deca.nb_competences if deca else 0, } # --- Les RCUEs rcue_list = [] if deca: for rcue in deca.rcues_annee: dec_rcue = deca.dec_rcue_by_ue.get(rcue.ue_1.id) if dec_rcue is not None: # None si l'UE n'est pas associée à un niveau dec_ue1 = deca.decisions_ues[rcue.ue_1.id] dec_ue2 = deca.decisions_ues[rcue.ue_2.id] rcue_dict = { "ue_1": { "ue_id": rcue.ue_1.id, "moy": None if (dec_ue1.moy_ue is None or np.isnan(dec_ue1.moy_ue)) else dec_ue1.moy_ue, "code": dec_ue1.code_valide, }, "ue_2": { "ue_id": rcue.ue_2.id, "moy": None if (dec_ue2.moy_ue is None or np.isnan(dec_ue2.moy_ue)) else dec_ue2.moy_ue, "code": dec_ue2.code_valide, }, "moy": rcue.moy_rcue, "code": dec_rcue.code_valide, } rcue_list.append(rcue_dict) row["rcues"] = rcue_list # --- Les UEs ue_list = [] if dec_etud["decisions_ue"]: for ue_id, ue_dec in dec_etud["decisions_ue"].items(): ue_dict = { "ue_id": ue_id, "code": ue_dec["code"], "ects": ue_dec["ects"], } ue_list.append(ue_dict) row["ues"] = ue_list # --- Le semestre (pour les formations classiques) if dec_etud["decision_sem"]: row["semestre"] = {"code": dec_etud["decision_sem"].get("code")} else: row["semestre"] = {} # APC, ... # --- Autorisations row["autorisations"] = dec_etud["autorisations"] return row