############################################################################## # ScoDoc # Copyright (c) 1999 - 2023 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.but import jury_but from app.but.jury_but import ( DecisionsProposeesAnnee, DecisionsProposeesRCUE, DecisionsProposeesUE, ) from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_compat import NotesTableCompat from app.comp import res_sem from app.models import UniteEns 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_RCUE_SUFFISANT, ) from app.scodoc import sco_formsemestre_status 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 - le lien de saisie ou modif de la décision de jury """ def __init__(self, *args, row_class: str = None, read_only=True, **kwargs): 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_rcues() self.add_jury() self.add_groups_header() # Termine la table self.finalize() 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 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_rcue_cols(dec_rcue) def add_jury(self): """Ajoute la colonne code jury 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. """ res = self.res 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""" 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 res.formsemestre.etat else "voir"} décisions""", group="col_jury_link", classes=["fontred"] if a_saisir else [], 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"}, ) 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" 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.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 ""), 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_decision_ues(self.etud.id) if not dues: return due = dues.get(ue.id) if not due: return col_id = f"moy_ue_{ue.id}_code" self.add_cell( col_id, "", # titre vide due["code"], raw_content=due["code"], group="col_ue", classes=["recorded_code"], column_classes={"col_jury", "col_ue_code"}, target_attrs={ "title": f"""enregistrée le {due['event_date']}, { (due["ects"] or 0):.3g} ECTS.""" }, ) def add_rcue_cols(self, dec_rcue: DecisionsProposeesRCUE): "2 cells: moyenne du RCUE, code enregistré" self.table.group_titles["rcue"] = "RCUEs en cours" 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.table.fmt_note(val), raw_content=val, group="rcue", classes=[note_class], column_classes={"col_rcue"}, ) self.add_cell( col_id + "_code", f"
{rcue.ue_1.acronyme}
{rcue.ue_2.acronyme}
", 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"}, ) # # --- 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", # ) def formsemestre_saisie_jury_but( formsemestre: 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. """ # 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 formsemestre.formation.referentiel_competence is None: raise ScoNoReferentielCompetences(formation=formsemestre.formation) res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre) table = TableJury( res, convert_values=True, mode_jury=True, read_only=read_only, classes=[ "table_jury_but_bilan" if mode == "recap" else "", "table_recap", "apc", "jury table_jury_but", ], selected_row_id=selected_etudid, ) if table.is_empty(): return ( '
aucun étudiant !
' ) table.data["filename"] = scu.sanitize_filename( f"""jury-but-{formsemestre.titre_num()}-{time.strftime("%Y-%m-%d")}""" ) table_html = table.html() H = [ html_sco_header.sco_header( page_title=f"{formsemestre.sem_modalite()}: jury BUT", no_side_bar=True, init_qtip=True, javascripts=["js/etud_info.js", "js/table_recap.js"], ), sco_formsemestre_status.formsemestre_status_head( formsemestre_id=formsemestre.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(table.freq_codes_annuels.values())} / {len(table)}
Codes annuels octroyés:
""" ) for code in sorted(table.freq_codes_annuels.keys()): H.append( f"""""" ) H.append( f"""
{code} {table.freq_codes_annuels[code]} { (100*table.freq_codes_annuels[code] / len(table)):2.1f}%
{html_sco_header.sco_footer()} """ ) return "\n".join(H)