diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py index 033d625e..b7673ffe 100644 --- a/app/api/formsemestres.py +++ b/app/api/formsemestres.py @@ -477,11 +477,8 @@ def formsemestre_resultat(formsemestre_id: int): formsemestre: FormSemestre = query.first_or_404(formsemestre_id) app.set_sco_dept(formsemestre.departement.acronym) res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) - table = res.get_table_recap( - convert_values=convert_values, - include_evaluations=False, - mode_jury=False, - allow_html=False, + table = TableRecap( + res, convert_values=convert_values, include_evaluations=False, mode_jury=False ) # Supprime les champs inutiles (mise en forme) rows = table.to_list() diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 17658ffa..1e861c9e 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -650,10 +650,13 @@ class DecisionsProposeesAnnee(DecisionsProposees): à poursuivre après le semestre courant. """ # La poursuite d'études dans un semestre pair d’une même année - # est de droit pour tout étudiant: - if (self.formsemestre.semestre_id % 2) and sco_codes.CursusBUT.NB_SEM: - ids.add(self.formsemestre.semestre_id + 1) - + # est de droit pour tout étudiant. + # Pas de redoublements directs de S_impair vers S_impair + # (pourront être traités manuellement) + if ( + self.formsemestre.semestre_id % 2 + ) and self.formsemestre.semestre_id < sco_codes.CursusBUT.NB_SEM: + return {self.formsemestre.semestre_id + 1} # La poursuite d’études dans un semestre impair est possible si # et seulement si l’étudiant a obtenu : # - la moyenne à plus de la moitié des regroupements cohérents d’UE ; @@ -667,7 +670,7 @@ class DecisionsProposeesAnnee(DecisionsProposees): if ( self.jury_annuel and code in sco_codes.BUT_CODES_PASSAGE - and self.formsemestre_pair.semestre_id < sco_codes.CursusBUT.NB_SEM + and self.formsemestre.semestre_id < sco_codes.CursusBUT.NB_SEM ): ids.add(self.formsemestre.semestre_id + 1) diff --git a/app/but/jury_but_pv.py b/app/but/jury_but_pv.py index 2a1a6ac1..e3119b27 100644 --- a/app/but/jury_but_pv.py +++ b/app/but/jury_but_pv.py @@ -97,12 +97,17 @@ def pvjury_page_but(formsemestre_id: int, fmt="html"): def pvjury_table_but( - formsemestre: FormSemestre, etudids: list[int] = None, line_sep: str = "\n" + formsemestre: FormSemestre, + etudids: list[int] = None, + line_sep: str = "\n", + only_diplome=False, + anonymous=False, + with_paragraph_nom=False, ) -> tuple[list[dict], dict]: """Table avec résultats jury BUT pour PV. Si etudids est None, prend tous les étudiants inscrits. """ - # remplace pour le BUT la fonction sco_pvjury.pvjury_table + # remplace pour le BUT la fonction sco_pv_forms.pvjury_table annee_but = (formsemestre.semestre_id + 1) // 2 titles = { "nom": "Code" if anonymous else "Nom", diff --git a/app/but/jury_but_recap.py b/app/but/jury_but_recap.py deleted file mode 100644 index 2e56ca3c..00000000 --- a/app/but/jury_but_recap.py +++ /dev/null @@ -1,565 +0,0 @@ -############################################################################## -# 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 diff --git a/app/but/jury_but_results.py b/app/but/jury_but_results.py index bf273b9d..00aa649a 100644 --- a/app/but/jury_but_results.py +++ b/app/but/jury_but_results.py @@ -12,7 +12,7 @@ import numpy as np from app.but import jury_but from app.models.etudiants import Identite from app.models.formsemestre import FormSemestre -from app.scodoc import sco_dict_pv_jury +from app.scodoc import sco_pv_dict def get_jury_but_results(formsemestre: FormSemestre) -> list[dict]: @@ -20,7 +20,7 @@ def get_jury_but_results(formsemestre: FormSemestre) -> list[dict]: 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_dict_pv_jury.dict_pvjury(formsemestre.id) + dpv = sco_pv_dict.dict_pvjury(formsemestre.id) rows = [] for etudid in formsemestre.etuds_inscriptions: rows.append(_get_jury_but_etud_result(formsemestre, dpv, etudid)) diff --git a/app/comp/moy_ue.py b/app/comp/moy_ue.py index 80c665f2..bf158669 100644 --- a/app/comp/moy_ue.py +++ b/app/comp/moy_ue.py @@ -33,10 +33,7 @@ import pandas as pd from app import db from app import models from app.models import ( - DispenseUE, FormSemestre, - FormSemestreInscription, - Identite, Module, ModuleImpl, ModuleUECoef, @@ -215,31 +212,6 @@ def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple: ) -def load_dispense_ues( - formsemestre: FormSemestre, etudids: pd.Index, ues: list[UniteEns] -) -> set[tuple[int, int]]: - """Construit l'ensemble des - etudids = modimpl_inscr_df.index, # les etudids - ue_ids : modimpl_coefs_df.index, # les UE du formsemestre sans les UE bonus sport - - Résultat: set de (etudid, ue_id). - """ - dispense_ues = set() - ue_sem_by_code = {ue.ue_code: ue for ue in ues} - # Prend toutes les dispenses obtenues par des étudiants de ce formsemestre, - # puis filtre sur inscrits et code d'UE UE - for dispense_ue in DispenseUE.query.join( - Identite, FormSemestreInscription - ).filter_by(formsemestre_id=formsemestre.id): - if dispense_ue.etudid in etudids: - # UE dans le semestre avec même code ? - ue = ue_sem_by_code.get(dispense_ue.ue.ue_code) - if ue is not None: - dispense_ues.add((dispense_ue.etudid, ue.id)) - - return dispense_ues - - def compute_ue_moys_apc( sem_cube: np.array, etuds: list, diff --git a/app/comp/res_but.py b/app/comp/res_but.py index 6a881cda..c65fabc7 100644 --- a/app/comp/res_but.py +++ b/app/comp/res_but.py @@ -74,7 +74,7 @@ class ResultatsSemestreBUT(NotesTableCompat): modimpl.module.ue.type != UE_SPORT for modimpl in self.formsemestre.modimpls_sorted ] - self.dispense_ues = moy_ue.load_dispense_ues( + self.dispense_ues = DispenseUE.load_formsemestre_dispense_ues_set( self.formsemestre, self.modimpl_inscr_df.index, self.ues ) self.etud_moy_ue = moy_ue.compute_ue_moys_apc( diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 3592385e..3af4786d 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -28,7 +28,6 @@ from app.scodoc.sco_cache import ResultatsSemestreCache from app.scodoc.codes_cursus import UE_SPORT from app.scodoc.sco_exceptions import ScoValueError from app.scodoc import sco_utils as scu -from app.scodoc import table_builder as tb # Il faut bien distinguer # - ce qui est caché de façon persistente (via redis): @@ -454,713 +453,3 @@ class ResultatsSemestre(ResultatsCache): # ici si l'étudiant est inscrit dans le semestre courant, # somme des coefs des modules de l'UE auxquels il est inscrit return self.compute_etud_ue_coef(etudid, ue) - - -# --- TABLEAU RECAP - - -class TableRecap(tb.Table): # was get_table_recap - """Table récap. des résultats. - allow_html: si vrai, peut mettre du HTML dans les valeurs - - Result: Table, le contenu étant une ligne par étudiant. - - - Si convert_values, transforme les notes en chaines ("12.34"). - Les colonnes générées sont: - etudid - rang : rang indicatif (basé sur moy gen) - moy_gen : moy gen indicative - moy_ue_, ..., les moyennes d'UE - moy_res__, ... les moyennes de ressources dans l'UE - moy_sae__, ... les moyennes de SAE dans l'UE - - On ajoute aussi des classes: - - pour les lignes: - selected_row pour l'étudiant sélectionné - - les colonnes: - - la moyenne générale a la classe col_moy_gen - - les colonnes SAE ont la classe col_sae - - les colonnes Resources ont la classe col_res - - les colonnes d'UE ont la classe col_ue - - les colonnes de modules (SAE ou res.) d'une UE ont la classe mod_ue_ - """ - - def __init__( - self, - res: ResultatsSemestre, - convert_values=False, - include_evaluations=False, - mode_jury=False, - ): - self.res = res - self.include_evaluations = include_evaluations - self.mode_jury = mode_jury - - parcours = self.formsemestre.formation.get_parcours() - self.barre_moy = parcours.BARRE_MOY - scu.NOTES_TOLERANCE - self.barre_valid_ue = parcours.NOTES_BARRE_VALID_UE - self.barre_warning_ue = parcours.BARRE_UE_DISPLAY_WARNING - self.cache_nomcomplet = {} # cache uid : nomcomplet - if convert_values: - self.fmt_note = scu.fmt_note - else: - self.fmt_note = lambda x: x - # couples (modimpl, ue) effectivement présents dans la table: - self.modimpl_ue_ids = set() - - etuds_inscriptions = self.formsemestre.etuds_inscriptions - ues = self.formsemestre.query_ues(with_sport=True) # avec bonus - ues_sans_bonus = [ue for ue in ues if ue.type != UE_SPORT] - - for etudid in etuds_inscriptions: - etud = Identite.query.get(etudid) - row = RowRecap(self, etudid) - self.add_row(row) - self.recap_add_etud(row, etud) - self._recap_add_moyennes(row, etud, ues_sans_bonus) - - self.recap_add_partitions() - self.recap_add_cursus() - self._recap_add_admissions() - - # tri par rang croissant - if not self.formsemestre.block_moyenne_generale: - self.sort_rows(key=lambda row: row.rang_order) - else: - self.sort_rows(key=lambda row: row.nb_ues_validables, reverse=True) - - # Lignes footer (min, max, ects, apo, ...) - self.add_bottom_rows(ues_sans_bonus) - - # Evaluations: - if include_evaluations: - self.add_evaluations() - - self.mark_empty_cols() - self.add_type_row() - - def mark_empty_cols(self): - """Ajoute style "col_empty" aux colonnes de modules vides""" - # identifie les col. vides par la classe sur leur moyenne - row_moy = self.get_row_by_id("moy") - for col_id in self.column_ids: - cell: tb.Cell = row_moy.cells.get(col_id) - if cell and "col_empty" in cell.classes: - self.column_classes[col_id].append("col_empty") - - def add_type_row(self): - """Ligne avec la classe de chaque colonne recap.""" - # récupère le type à partir des classes css (hack...) - row_type = tb.BottomRow( - self, - "type_col", - left_title="Type col.", - left_title_col_ids=["prenom", "nom_short"], - category="bottom_infos", - classes=["bottom_info"], - ) - for col_id in self.column_ids: - group_name = self.column_group.get(col_id, "") - if group_name.startswith("col_"): - group_name = group_name[4:] - row_type.add_cell(col_id, None, group_name) - - def add_bottom_rows(self, ues): - """Les informations à mettre en bas de la table recap: - min, max, moy, ECTS, Apo.""" - res = self.res - # Ordre des lignes: Min, Max, Moy, Coef, ECTS, Apo - row_min = tb.BottomRow( - self, - "min", - left_title="Min.", - left_title_col_ids=["prenom", "nom_short"], - category="bottom_infos", - classes=["bottom_info"], - ) - row_max = tb.BottomRow( - self, - "max", - left_title="Max.", - left_title_col_ids=["prenom", "nom_short"], - category="bottom_infos", - classes=["bottom_info"], - ) - row_moy = tb.BottomRow( - self, - "moy", - left_title="Moy.", - left_title_col_ids=["prenom", "nom_short"], - category="bottom_infos", - classes=["bottom_info"], - ) - row_coef = tb.BottomRow( - self, - "coef", - left_title="Coef.", - left_title_col_ids=["prenom", "nom_short"], - category="bottom_infos", - classes=["bottom_info"], - ) - row_ects = tb.BottomRow( - self, - "ects", - left_title="ECTS", - left_title_col_ids=["prenom", "nom_short"], - category="bottom_infos", - classes=["bottom_info"], - ) - row_apo = tb.BottomRow( - self, - "apo", - left_title="Code Apogée", - left_title_col_ids=["prenom", "nom_short"], - category="bottom_infos", - classes=["bottom_info"], - ) - - # --- ECTS - # titre (à gauche) sur 2 colonnes pour s'adapter à l'affichage des noms/prenoms - for ue in ues: - col_id = f"moy_ue_{ue.id}" - row_ects.add_cell(col_id, None, ue.ects) - # ajoute cell UE vides sur ligne coef pour borders verticales - # XXX TODO classes dans table sur colonne ajoutées à tous les TD - row_coef.add_cell(col_id, None, "") - row_ects.add_cell( - "moy_gen", - None, - sum([ue.ects or 0 for ue in ues if ue.type != UE_SPORT]), - ) - # --- MIN, MAX, MOY, APO - row_min.add_cell("moy_gen", None, self.fmt_note(res.etud_moy_gen.min())) - row_max.add_cell("moy_gen", None, self.fmt_note(res.etud_moy_gen.max())) - row_moy.add_cell("moy_gen", None, self.fmt_note(res.etud_moy_gen.mean())) - - for ue in ues: - col_id = f"moy_ue_{ue.id}" - row_min.add_cell(col_id, None, self.fmt_note(res.etud_moy_ue[ue.id].min())) - row_max.add_cell(col_id, None, self.fmt_note(res.etud_moy_ue[ue.id].max())) - row_moy.add_cell(col_id, None, self.fmt_note(res.etud_moy_ue[ue.id].mean())) - row_apo.add_cell(col_id, None, ue.code_apogee or "") - - for modimpl in res.formsemestre.modimpls_sorted: - if (modimpl.id, ue.id) in self.modimpl_ue_ids: - col_id = f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}" - if res.is_apc: - coef = res.modimpl_coefs_df[modimpl.id][ue.id] - else: - coef = modimpl.module.coefficient or 0 - row_coef.add_cell( - col_id, - None, - self.fmt_note(coef), - group=f"col_ue_{ue.id}_modules", - ) - notes = res.modimpl_notes(modimpl.id, ue.id) - if np.isnan(notes).all(): - # aucune note valide - row_min.add_cell(col_id, None, np.nan) - row_max.add_cell(col_id, None, np.nan) - moy = np.nan - else: - row_min.add_cell(col_id, None, self.fmt_note(np.nanmin(notes))) - row_max.add_cell(col_id, None, self.fmt_note(np.nanmax(notes))) - moy = np.nanmean(notes) - row_moy.add_cell( - col_id, - None, - self.fmt_note(moy), - # aucune note dans ce module ? - classes=["col_empty" if np.isnan(moy) else ""], - ) - row_apo.add_cell(col_id, None, modimpl.module.code_apogee or "") - - -class RowRecap(tb.Row): - "Ligne de la table recap" - - def add_etud(self, etud: Identite): - """Ajoute colonnes étudiant: codes, noms""" - res = self.table.res - # --- Codes (seront cachés, mais exportés en excel) - self.add_cell("etudid", "etudid", etud.id, "etud_codes") - self.add_cell( - "code_nip", - "code_nip", - etud.code_nip or "", - "etud_codes", - ) - - # --- Rang - if not res.formsemestre.block_moyenne_generale: - self.rang_order = res.etud_moy_gen_ranks_int[etud.id] - res.add_cell( - "rang", - "Rg", - self.etud_moy_gen_ranks[etud.id], - "rang", - data={"order": f"{self.rang_order:05d}"}, - ) - else: - self.rang_order = -1 - # --- Identité étudiant - url_bulletin = url_for( - "notes.formsemestre_bulletinetud", - scodoc_dept=g.scodoc_dept, - formsemestre_id=res.formsemestre.id, - etudid=etud.id, - ) - self.add_cell("civilite_str", "Civ.", etud.civilite_str, "identite_detail") - self.add_cell( - "nom_disp", - "Nom", - etud.nom_disp(), - "identite_detail", - data={"order": etud.sort_key}, - target=url_bulletin, - target_attrs={"class": "etudinfo", "id": str(etud.id)}, - ) - self.add_cell("prenom", "Prénom", etud.prenom, "identite_detail") - self.add_cell( - "nom_short", - "Nom", - etud.nom_short, - "identite_court", - data={ - "order": etud.sort_key, - "etudid": etud.id, - "nomprenom": etud.nomprenom, - }, - target=url_bulletin, - target_attrs={"class": "etudinfo", "id": str(etud.id)}, - ) - - def add_moyennes( # XXX was _recap_add_moyennes - self, - row: tb.Row, - etud: Identite, - ues_sans_bonus: list[UniteEns], - ): - """Ajoute cols moy_gen moy_ue et tous les modules...""" - table = self.table - res = table.res - # --- Moyenne générale - if not res.formsemestre.block_moyenne_generale: - moy_gen = res.etud_moy_gen.get(etud.id, False) - note_class = "" - if moy_gen is False: - moy_gen = scu.NO_NOTE_STR - elif isinstance(moy_gen, float) and moy_gen < table.barre_moy: - note_class = "moy_ue_warning" # en rouge - row.add_cell( - "moy_gen", - "Moy", - table.fmt_note(moy_gen), - "col_moy_gen", - classes=[note_class], - ) - # Ajoute bulle sur titre du pied de table: - if res.is_apc: - table.foot_title_row.cells["moy_gen"].target_attrs[ - "title" - ] = "moyenne indicative" - - # --- Moyenne d'UE - self.nb_ues_validables, self.nb_ues_warning = 0, 0 - for ue in ues_sans_bonus: - ue_status = res.get_etud_ue_status(etud.id, ue.id) - if ue_status is not None: - self.add_ue(ue, ue_status) - if table.mode_jury: - # pas d'autre colonnes de résultats - continue - - # Bonus (sport) dans cette UE ? - # Le bonus sport appliqué sur cette UE - if (res.bonus_ues is not None) and (ue.id in res.bonus_ues): - val = res.bonus_ues[ue.id][etud.id] or "" - val_fmt = val_fmt_html = table.fmt_note(val) - if val: - val_fmt_html = f"""{ - val_fmt - }""" - row.add_cell( - f"bonus_ue_{ue.id}", - f"Bonus {ue.acronyme}", - val_fmt_html, - raw_content=val_fmt, - group=f"col_ue_{ue.id}", - classes=["col_ue_bonus"], - ) - # Les moyennes des modules (ou ressources et SAÉs) dans cette UE - self.add_ue_modimpls( - ue, etud, ue_status["is_capitalized"] - ) # XXX _recap_add_ue_modimpls - - self.nb_ues_etud_parcours = len(res.etud_ues_ids(etud.id)) - ue_valid_txt = ( - ue_valid_txt_html - ) = f"{self.nb_ues_validables}/{self.nb_ues_etud_parcours}" - if self.nb_ues_warning: - ue_valid_txt_html += " " + scu.EMO_WARNING - # place juste avant moy. gen. - table.insert_group("col_ues_validables", before="col_moy_gen") - classes = ["col_ue"] - if self.nb_ues_warning: - classes.append("moy_ue_warning") - elif self.nb_ues_validables < len(ues_sans_bonus): - classes.append("moy_inf") - row.add_cell( - "ues_validables", - "UEs", - ue_valid_txt_html, - "col_ues_validables", - classes=classes, - raw_content=ue_valid_txt, - data={"order": self.nb_ues_validables}, # tri - ) - - if table.mode_jury and res.validations: - if res.is_apc: - # formations BUT: pas de code semestre, concatene ceux des UEs - dec_ues = res.validations.decisions_jury_ues.get(etud.id) - if dec_ues: - jury_code_sem = ",".join( - [dec_ues[ue_id].get("code", "") for ue_id in dec_ues] - ) - else: - jury_code_sem = "" - else: - # formations classiques: code semestre - dec_sem = res.validations.decisions_jury.get(etud.id) - jury_code_sem = dec_sem["code"] if dec_sem else "" - self.add_cell("jury_code_sem", "Jury", jury_code_sem, "jury_code_sem") - self.add_cell( - "jury_link", - "", - f"""{("saisir" if not jury_code_sem else "modifier") - if res.formsemestre.etat else "voir"} décisions""", - "col_jury_link", - ) - - def add_ue(self, ue: UniteEns, ue_status: dict): - "Ajoute résultat UE au row (colonne col_ue)" - table = self.table - col_id = f"moy_ue_{ue.id}" - val = ue_status["moy"] - note_class = "" - if isinstance(val, float): - if val < table.barre_moy: - note_class = "moy_inf" - elif val >= table.barre_valid_ue: - note_class = "moy_ue_valid" - self.nb_ues_validables += 1 - if val < table.barre_warning_ue: - note_class = "moy_ue_warning" # notes très basses - self.nb_ues_warning += 1 - self.add_cell( - col_id, - ue.acronyme, - table.fmt_note(val), - group=f"col_ue_{ue.id}", - classes=["col_ue", note_class], - ) - table.foot_title_row.cells[col_id].target_attrs[ - "title" - ] = f"""{ue.titre} S{ue.semestre_idx or '?'}""" - - def add_ue_modimpls( - self, row: tb.Row, ue: UniteEns, etud: Identite, is_capitalized: bool - ): - """Ajoute à row les moyennes des modules (ou ressources et SAÉs) dans l'UE""" - # pylint: disable=invalid-unary-operand-type - table = row.table - res = table.res - for modimpl in res.modimpls_in_ue(ue, etud.id, with_bonus=False): - if is_capitalized: - val = "-c-" - else: - modimpl_results = res.modimpls_results.get(modimpl.id) - if modimpl_results: # pas bonus - if res.is_apc: # BUT - moys_vers_ue = modimpl_results.etuds_moy_module.get(ue.id) - val = ( - moys_vers_ue.get(etud.id, "?") - if moys_vers_ue is not None - else "" - ) - else: # classique: Series indépendante de l'UE - val = modimpl_results.etuds_moy_module.get(etud.id, "?") - else: - val = "" - - col_id = f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}" - val_fmt = val_fmt_html = table.fmt_note(val) - if modimpl.module.module_type == scu.ModuleType.MALUS: - if val and not isinstance(val, str) and not np.isnan(val): - if val >= 0: - val_fmt_html = f"""+{ - val_fmt - }""" - else: - # val_fmt_html = (scu.EMO_RED_TRIANGLE_DOWN + val_fmt) - val_fmt_html = f"""-{ - table.fmt_note(-val)}""" - - else: - val_fmt = val_fmt_html = "" # inscrit à ce malus, mais sans note - - cell = self.add_cell( - col_id, - modimpl.module.code, - val_fmt_html, - raw_content=val_fmt, - group=f"col_ue_{ue.id}_modules", - classes=[ - f"col_{modimpl.module.type_abbrv()}", - f"mod_ue_{ue.id}", - ], - ) - if modimpl.module.module_type == scu.ModuleType.MALUS: - # positionne la colonne à droite de l'UE - cell.group = f"col_ue_{ue.id}_malus" - table.insert_group(cell.group, after=f"col_ue_{ue.id}") - - table.foot_title_row.cells[col_id].target = url_for( - "notes.moduleimpl_status", - scodoc_dept=g.scodoc_dept, - moduleimpl_id=modimpl.id, - ) - - nom_resp = table.cache_nomcomplet.get(modimpl.responsable_id) - if nom_resp is None: - user = User.query.get(modimpl.responsable_id) - nom_resp = user.get_nomcomplet() if user else "" - table.cache_nomcomplet[modimpl.responsable_id] = nom_resp - table.foot_title_row.cells[col_id].target_attrs[ - "title" - ] = f"{modimpl.module.titre} ({nom_resp})" - table.modimpl_ue_ids.add((modimpl.id, ue.id)) - - def _recap_etud_groups_infos( - self, etudid: int, row: dict, titles: dict - ): # XXX non utilisé - """Table recap: ajoute à row les colonnes sur les groupes pour cet etud""" - # dec = self.get_etud_decision_sem(etudid) - # if dec: - # codes_nb[dec["code"]] += 1 - row_class = "" - etud_etat = self.get_etud_etat(etudid) - if etud_etat == DEM: - gr_name = "Dém." - row_class = "dem" - elif etud_etat == DEF: - gr_name = "Déf." - row_class = "def" - else: - # XXX probablement à revoir pour utiliser données cachées, - # via get_etud_groups_in_partition ou autre - group = sco_groups.get_etud_main_group(etudid, self.formsemestre.id) - gr_name = group["group_name"] or "" - row["group"] = gr_name - row["_group_class"] = "group" - if row_class: - row["_tr_class"] = " ".join([row.get("_tr_class", ""), row_class]) - titles["group"] = "Gr" - - def _recap_add_admissions(self, table: tb.Table): - """Ajoute les colonnes "admission" - Les colonnes ont la classe css "admission" - """ - fields = { - "bac": "Bac", - "specialite": "Spécialité", - "type_admission": "Type Adm.", - "classement": "Rg. Adm.", - } - first = True - for cid, title in fields.items(): - cell_head, cell_foot = table.add_title(cid, title) - cell_head.classes.append("admission") - cell_foot.classes.append("admission") - if first: - cell_head.classes.append("admission_first") - cell_foot.classes.append("admission_first") - first = False - - for row in table.rows: - etud = Identite.query.get(row.id) # TODO XXX - admission = etud.admission.first() - first = True - for cid, title in fields.items(): - cell = row.add_cell( - cid, - title, - getattr(admission, cid) or "", - "admission", - ) - if first: - cell.classes.append("admission_first") - first = False - - def recap_add_cursus(self, table: tb.Table): - """Ajoute colonne avec code cursus, eg 'S1 S2 S1'""" - table.insert_group("cursus", before="col_ues_validables") - cid = "code_cursus" - formation_code = self.formsemestre.formation.formation_code - for row in table.rows: - etud = Identite.query.get(row.id) # TODO XXX à optimiser: etud dans row - row.add_cell( - cid, - "Cursus", - " ".join( - [ - f"S{ins.formsemestre.semestre_id}" - for ins in reversed(etud.inscriptions()) - if ins.formsemestre.formation.formation_code == formation_code - ] - ), - "cursus", - ) - - def recap_add_partitions(self, table: tb.Table): - """Ajoute les colonnes indiquant les groupes - La table contient des rows avec la clé etudid. - - Les colonnes ont la classe css "partition" - """ - table.insert_group("partition", after="identite_court") - partitions, partitions_etud_groups = sco_groups.get_formsemestre_groups( - self.formsemestre.id - ) - first_partition = True - for partition in partitions: - col_classes = [] # la classe "partition" sera ajoutée par la table - if not first_partition: - col_classes.append("partition_aux") - first_partition = False - cid = f"part_{partition['partition_id']}" - cell_head, cell_foot = table.add_title(cid, partition["partition_name"]) - cell_head.classes += col_classes - cell_foot.classes += col_classes - - if partition["bul_show_rank"]: - rg_cid = cid + "_rg" # rang dans la partition - cell_head, cell_foot = table.add_title( - cid, f"Rg {partition['partition_name']}" - ) - cell_head.classes.append("partition_rangs") - cell_foot.classes.append("partition_rangs") - - partition_etud_groups = partitions_etud_groups[partition["partition_id"]] - for row in table.rows: - etudid = row.id - group = None # group (dict) de l'étudiant dans cette partition - # dans NotesTableCompat, à revoir - etud_etat = self.get_etud_etat(row.id) # row.id == etudid - tr_classes = [] - if etud_etat == scu.DEMISSION: - gr_name = "Dém." - tr_classes.append("dem") - elif etud_etat == DEF: - gr_name = "Déf." - tr_classes.append("def") - else: - group = partition_etud_groups.get(etudid) - gr_name = group["group_name"] if group else "" - if gr_name: - row.add_cell( - cid, - partition["partition_name"], - gr_name, - "partition", - classes=col_classes, - ) - - # Rangs dans groupe - if ( - partition["bul_show_rank"] - and (group is not None) - and (group["id"] in self.moy_gen_rangs_by_group) - ): - rang = self.moy_gen_rangs_by_group[group["id"]][0] - row.add_cell(rg_cid, None, rang.get(etudid, ""), "partition") - - def _recap_add_evaluations(self, table: tb.Table): - """Ajoute les colonnes avec les notes aux évaluations - rows est une liste de dict avec une clé "etudid" - Les colonnes ont la classe css "evaluation" - """ - # nouvelle ligne pour description évaluations: - row_descr_eval = tb.BottomRow( - table, - "evaluations", - left_title="Description évaluations", - left_title_col_ids=["prenom", "nom_short"], - category="bottom_infos", - classes=["bottom_info"], - ) - - first_eval = True - for modimpl in self.formsemestre.modimpls_sorted: - evals = self.modimpls_results[modimpl.id].get_evaluations_completes(modimpl) - eval_index = len(evals) - 1 - inscrits = {i.etudid for i in modimpl.inscriptions} - first_eval_of_mod = True - for e in evals: - col_id = f"eval_{e.id}" - title = f'{modimpl.module.code} {eval_index} {e.jour.isoformat() if e.jour else ""}' - col_classes = ["evaluation"] - if first_eval: - col_classes.append("first") - elif first_eval_of_mod: - col_classes.append("first_of_mod") - first_eval_of_mod = first_eval = False - eval_index -= 1 - notes_db = sco_evaluation_db.do_evaluation_get_all_notes( - e.evaluation_id - ) - for row in table.rows: - etudid = row.id - if etudid in inscrits: - if etudid in notes_db: - val = notes_db[etudid]["value"] - else: - # Note manquante mais prise en compte immédiate: affiche ATT - val = scu.NOTES_ATTENTE - content = table.fmt_note(val) - classes = col_classes + [ - { - "ABS": "abs", - "ATT": "att", - "EXC": "exc", - }.get(content, "") - ] - row.add_cell(col_id, title, content, "", classes=classes) - else: - row.add_cell( - col_id, - title, - "ni", - "", - classes=col_classes + ["non_inscrit"], - ) - - table.get_row_by_id("coef").row[col_id] = e.coefficient - table.get_row_by_id("min").row[col_id] = "0" - table.get_row_by_id("max").row[col_id] = table.fmt_note(e.note_max) - row_descr_eval.add_cell( - col_id, - None, - e.description or "", - target=url_for( - "notes.evaluation_listenotes", - scodoc_dept=g.scodoc_dept, - evaluation_id=e.id, - ), - ) diff --git a/app/models/ues.py b/app/models/ues.py index b0bf0854..faa5df20 100644 --- a/app/models/ues.py +++ b/app/models/ues.py @@ -260,12 +260,23 @@ class UniteEns(db.Model): class DispenseUE(db.Model): """Dispense d'UE - Utilisé en PCC (BUT) pour indiquer les étudiants redoublants avec une UE capitalisée + Utilisé en APC (BUT) pour indiquer les étudiants redoublants avec une UE capitalisée qu'ils ne refont pas. + La dispense d'UE n'est PAS une validation: + - elle n'est pas affectée par les décisions de jury (pas effacée) + - elle est associée à un formsemestre + - elle ne permet pas la délivrance d'ECTS ou du diplôme. + + On utilise cette dispense et non une "inscription" par souci d'efficacité: + en général, la grande majorité des étudiants suivront toutes les UEs de leur parcours, + la dispense étant une exception. """ - __table_args__ = (db.UniqueConstraint("ue_id", "etudid"),) + __table_args__ = (db.UniqueConstraint("formsemestre_id", "ue_id", "etudid"),) id = db.Column(db.Integer, primary_key=True) + formsemestre_id = formsemestre_id = db.Column( + db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, nullable=True + ) ue_id = db.Column( db.Integer, db.ForeignKey(UniteEns.id, ondelete="CASCADE"), @@ -284,3 +295,25 @@ class DispenseUE(db.Model): def __repr__(self) -> str: return f"""<{self.__class__.__name__} {self.id} etud={ repr(self.etud)} ue={repr(self.ue)}>""" + + @classmethod + def load_formsemestre_dispense_ues_set( + cls, formsemestre: "FormSemestre", etudids: pd.Index, ues: list[UniteEns] + ) -> set[tuple[int, int]]: + """Construit l'ensemble des + etudids = modimpl_inscr_df.index, # les etudids + ue_ids : modimpl_coefs_df.index, # les UE du formsemestre sans les UE bonus sport + + Résultat: set de (etudid, ue_id). + """ + # Prend toutes les dispenses obtenues par des étudiants de ce formsemestre, + # puis filtre sur inscrits et ues + ue_ids = {ue.id for ue in ues} + dispense_ues = { + (dispense_ue.etudid, dispense_ue.ue_id) + for dispense_ue in DispenseUE.query.filter_by( + formsemestre_id=formsemestre.id + ) + if dispense_ue.etudid in etudids and dispense_ue.ue_id in ue_ids + } + return dispense_ues diff --git a/app/pe/pe_jurype.py b/app/pe/pe_jurype.py index 69258517..a18626d6 100644 --- a/app/pe/pe_jurype.py +++ b/app/pe/pe_jurype.py @@ -65,10 +65,8 @@ def comp_nom_semestre_dans_parcours(sem): """Le nom a afficher pour titrer un semestre par exemple: "semestre 2 FI 2015" """ - from app.scodoc import sco_formations - - F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0] - parcours = codes_cursus.get_cursus_from_code(F["type_parcours"]) + formation: Formation = Formation.query.get_or_404(sem["formation_id"]) + parcours = codes_cursus.get_cursus_from_code(formation.type_parcours) return "%s %s %s %s" % ( parcours.SESSION_NAME, # eg "semestre" sem["semestre_id"], # eg 2 diff --git a/app/scodoc/codes_cursus.py b/app/scodoc/codes_cursus.py index 001a1b8a..e7af230d 100644 --- a/app/scodoc/codes_cursus.py +++ b/app/scodoc/codes_cursus.py @@ -824,7 +824,8 @@ FORMATION_CURSUS_DESCRS = [p[1].__doc__ for p in _tp] # intitulés (eg pour men FORMATION_CURSUS_TYPES = [p[0] for p in _tp] # codes numeriques (TYPE_CURSUS) -def get_cursus_from_code(code_cursus): +def get_cursus_from_code(code_cursus: int) -> TypeCursus: + "renvoie le cursus de code indiqué" cursus = SCO_CURSUS.get(code_cursus) if cursus is None: log(f"Warning: invalid code_cursus: {code_cursus}") diff --git a/app/scodoc/notes_table.py b/app/scodoc/notes_table.py deleted file mode 100644 index 0379bbc2..00000000 --- a/app/scodoc/notes_table.py +++ /dev/null @@ -1,1356 +0,0 @@ -# -*- 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 -# -############################################################################## - -"""Calculs sur les notes et cache des résultats - - Ancien code ScoDoc 7 en cours de rénovation -""" - -from operator import itemgetter - -from flask import g, url_for - -from app.but import bulletin_but -from app.models import FormSemestre, Identite -from app.models import ScoDocSiteConfig -import app.scodoc.sco_utils as scu -from app.scodoc.sco_utils import ModuleType -import app.scodoc.notesdb as ndb -from app import log -from app.scodoc.sco_formulas import NoteVector -from app.scodoc.sco_exceptions import ScoValueError - -from app.scodoc.sco_formsemestre import ( - formsemestre_uecoef_list, - formsemestre_uecoef_create, -) -from app.scodoc.codes_cursus import ( - DEF, - UE_SPORT, - ue_is_fondamentale, - ue_is_professionnelle, -) -from app.scodoc import sco_cache -from app.scodoc import codes_cursus -from app.scodoc import sco_compute_moy -from app.scodoc.sco_cursus import formsemestre_get_etud_capitalisation -from app.scodoc import sco_cursus_dut -from app.scodoc import sco_edit_matiere -from app.scodoc import sco_edit_module -from app.scodoc import sco_edit_ue -from app.scodoc import sco_etud -from app.scodoc import sco_evaluations -from app.scodoc import sco_formations -from app.scodoc import sco_formsemestre -from app.scodoc import sco_formsemestre_inscriptions -from app.scodoc import sco_groups -from app.scodoc import sco_moduleimpl -from app.scodoc import sco_preferences - - -def comp_ranks(T): - """Calcul rangs à partir d'une liste ordonnée de tuples [ (valeur, ..., etudid) ] - (valeur est une note numérique), en tenant compte des ex-aequos - Le resultat est: { etudid : rang } où rang est une chaine decrivant le rang - """ - rangs = {} # { etudid : rang } (rang est une chaine) - nb_ex = 0 # nb d'ex-aequo consécutifs en cours - for i in range(len(T)): - # test ex-aequo - if i < len(T) - 1: - next = T[i + 1][0] - else: - next = None - moy = T[i][0] - if nb_ex: - srang = "%d ex" % (i + 1 - nb_ex) - if moy == next: - nb_ex += 1 - else: - nb_ex = 0 - else: - if moy == next: - srang = "%d ex" % (i + 1 - nb_ex) - nb_ex = 1 - else: - srang = "%d" % (i + 1) - rangs[T[i][-1]] = srang # str(i+1) - return rangs - - -def get_sem_ues_modimpls(formsemestre_id, modimpls=None): - """Get liste des UE du semestre (à partir des moduleimpls) - (utilisé quand on ne peut pas construire nt et faire nt.get_ues_stat_dict()) - """ - if modimpls is None: - modimpls = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id) - uedict = {} - for modimpl in modimpls: - mod = sco_edit_module.module_list(args={"module_id": modimpl["module_id"]})[0] - modimpl["module"] = mod - if not mod["ue_id"] in uedict: - ue = sco_edit_ue.ue_list(args={"ue_id": mod["ue_id"]})[0] - uedict[ue["ue_id"]] = ue - ues = list(uedict.values()) - ues.sort(key=lambda u: u["numero"]) - return ues, modimpls - - -def comp_etud_sum_coef_modules_ue(formsemestre_id, etudid, ue_id): - """Somme des coefficients des modules de l'UE dans lesquels cet étudiant est inscrit - ou None s'il n'y a aucun module. - - (nécessaire pour éviter appels récursifs de nt, qui peuvent boucler) - """ - infos = ndb.SimpleDictFetch( - """SELECT mod.coefficient - FROM notes_modules mod, notes_moduleimpl mi, notes_moduleimpl_inscription ins - WHERE mod.id = mi.module_id - and ins.etudid = %(etudid)s - and ins.moduleimpl_id = mi.id - and mi.formsemestre_id = %(formsemestre_id)s - and mod.ue_id = %(ue_id)s - """, - {"etudid": etudid, "formsemestre_id": formsemestre_id, "ue_id": ue_id}, - ) - - if not infos: - return None - else: - s = sum(x["coefficient"] for x in infos) - return s - - -class NotesTable: - """Une NotesTable représente un tableau de notes pour un semestre de formation. - Les colonnes sont des modules. - Les lignes des étudiants. - On peut calculer les moyennes par étudiant (pondérées par les coefs) - ou les moyennes par module. - - Attributs publics (en lecture): - - inscrlist: étudiants inscrits à ce semestre, par ordre alphabétique (avec demissions) - - identdict: { etudid : ident } - - sem : le formsemestre - get_table_moyennes_triees: [ (moy_gen, moy_ue1, moy_ue2, ... moy_ues, moy_mod1, ..., moy_modn, etudid) ] - (où toutes les valeurs sont soit des nombres soit des chaines spéciales comme 'NA', 'NI'), - incluant les UE de sport - - - bonus[etudid] : valeur du bonus "sport". - - Attributs privés: - - _modmoys : { moduleimpl_id : { etudid: note_moyenne_dans_ce_module } } - - _ues : liste des UE de ce semestre (hors capitalisees) - - _matmoys : { matiere_id : { etudid: note moyenne dans cette matiere } } - - """ - - def __init__(self, formsemestre_id): - # log(f"NotesTable( formsemestre_id={formsemestre_id} )") - raise NotImplementedError() # XXX - if not formsemestre_id: - raise ValueError("invalid formsemestre_id (%s)" % formsemestre_id) - self.formsemestre_id = formsemestre_id - cnx = ndb.GetDBConnexion() - self.sem = sco_formsemestre.get_formsemestre(formsemestre_id) - self.moduleimpl_stats = {} # { moduleimpl_id : {stats} } - self._uecoef = {} # { ue_id : coef } cache coef manuels ue cap - self._evaluations_etats = None # liste des evaluations avec état - self.use_ue_coefs = sco_preferences.get_preference( - "use_ue_coefs", formsemestre_id - ) - # si vrai, bloque calcul des moy gen. et d'UE.: - self.block_moyennes = self.sem["block_moyennes"] - # Infos sur les etudiants - self.inscrlist = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( - args={"formsemestre_id": formsemestre_id} - ) - # infos identite etudiant - # xxx sous-optimal: 1/select par etudiant -> 0.17" pour identdict sur GTR1 ! - self.identdict = {} # { etudid : ident } - self.inscrdict = {} # { etudid : inscription } - for x in self.inscrlist: - i = sco_etud.etudident_list(cnx, {"etudid": x["etudid"]})[0] - self.identdict[x["etudid"]] = i - self.inscrdict[x["etudid"]] = x - x["nomp"] = (i["nom_usuel"] or i["nom"]) + i["prenom"] # pour tri - - # Tri les etudids par NOM - self.inscrlist.sort(key=itemgetter("nomp")) - - # { etudid : rang dans l'ordre alphabetique } - self._rang_alpha = {e["etudid"]: i for i, e in enumerate(self.inscrlist)} - - self.bonus = scu.DictDefault(defaultvalue=0) - # Notes dans les modules { moduleimpl_id : { etudid: note_moyenne_dans_ce_module } } - ( - self._modmoys, - self._modimpls, - self._valid_evals_per_mod, - valid_evals, - mods_att, - self.expr_diagnostics, - ) = sco_compute_moy.formsemestre_compute_modimpls_moyennes( - self, formsemestre_id - ) - self._mods_att = mods_att # liste des modules avec des notes en attente - self._matmoys = {} # moyennes par matieres - self._valid_evals = {} # { evaluation_id : eval } - for e in valid_evals: - self._valid_evals[e["evaluation_id"]] = e # Liste des modules et UE - uedict = {} # public member: { ue_id : ue } - self.uedict = uedict # les ues qui ont un modimpl dans ce semestre - for modimpl in self._modimpls: - # module has been added by formsemestre_compute_modimpls_moyennes - mod = modimpl["module"] - if not mod["ue_id"] in uedict: - ue = sco_edit_ue.ue_list(args={"ue_id": mod["ue_id"]})[0] - uedict[ue["ue_id"]] = ue - else: - ue = uedict[mod["ue_id"]] - modimpl["ue"] = ue # add ue dict to moduleimpl - self._matmoys[mod["matiere_id"]] = {} - mat = sco_edit_matiere.matiere_list(args={"matiere_id": mod["matiere_id"]})[ - 0 - ] - modimpl["mat"] = mat # add matiere dict to moduleimpl - # calcul moyennes du module et stocke dans le module - # nb_inscrits, nb_notes, nb_abs, nb_neutre, moy, median, last_modif= - - self.formation = sco_formations.formation_list( - args={"formation_id": self.sem["formation_id"]} - )[0] - self.parcours = codes_cursus.get_cursus_from_code( - self.formation["type_parcours"] - ) - - # En APC, il faut avoir toutes les UE du semestre - # (elles n'ont pas nécessairement un module rattaché): - if self.parcours.APC_SAE: - formsemestre = FormSemestre.query.get(formsemestre_id) - for ue in formsemestre.query_ues(): - if ue.id not in self.uedict: - self.uedict[ue.id] = ue.to_dict() - - # Decisions jury et UE capitalisées - self.comp_decisions_jury() - self.comp_ue_capitalisees() - - # Liste des moyennes de tous, en chaines de car., triées - self._ues = list(uedict.values()) - self._ues.sort(key=lambda u: u["numero"]) - - T = [] - - self.moy_gen = {} # etudid : moy gen (avec UE capitalisées) - self.moy_ue = {} # ue_id : { etudid : moy ue } (valeur numerique) - self.etud_moy_infos = {} # etudid : resultats de comp_etud_moy_gen() - valid_moy = [] # liste des valeurs valides de moyenne generale (pour min/max) - for ue in self._ues: - self.moy_ue[ue["ue_id"]] = {} - self._etud_moy_ues = {} # { etudid : { ue_id : {'moy', 'sum_coefs', ... } } - - for etudid in self.get_etudids(): - etud_moy_gen = self.comp_etud_moy_gen(etudid, cnx) - self.etud_moy_infos[etudid] = etud_moy_gen - ue_status = etud_moy_gen["moy_ues"] - self._etud_moy_ues[etudid] = ue_status - - moy_gen = etud_moy_gen["moy"] - self.moy_gen[etudid] = moy_gen - if etud_moy_gen["sum_coefs"] > 0: - valid_moy.append(moy_gen) - - moy_ues = [] - for ue in self._ues: - moy_ue = ue_status[ue["ue_id"]]["moy"] - moy_ues.append(moy_ue) - self.moy_ue[ue["ue_id"]][etudid] = moy_ue - - t = [moy_gen] + moy_ues - # - is_cap = {} # ue_id : is_capitalized - for ue in self._ues: - is_cap[ue["ue_id"]] = ue_status[ue["ue_id"]]["is_capitalized"] - - for modimpl in self.get_modimpls_dict(): - val = self.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid) - if is_cap[modimpl["module"]["ue_id"]]: - t.append("-c-") - else: - t.append(val) - # - t.append(etudid) - T.append(t) - - self.T = T - # tri par moyennes décroissantes, - # en laissant les demissionnaires a la fin, par ordre alphabetique - self.T.sort(key=self._row_key) - - if len(valid_moy): - self.moy_min = min(valid_moy) - self.moy_max = max(valid_moy) - else: - self.moy_min = self.moy_max = "NA" - - # calcul rangs (/ moyenne generale) - self.etud_moy_gen_ranks = comp_ranks(T) - - self.rangs_groupes = ( - {} - ) # { group_id : { etudid : rang } } (lazy, see get_etud_rang_group) - self.group_etuds = ( - {} - ) # { group_id : set of etudids } (lazy, see get_etud_rang_group) - - # calcul rangs dans chaque UE - ue_rangs = ( - {} - ) # ue_rangs[ue_id] = ({ etudid : rang }, nb_inscrits) (rang est une chaine) - for ue in self._ues: - ue_id = ue["ue_id"] - val_ids = [ - (self.moy_ue[ue_id][etudid], etudid) for etudid in self.moy_ue[ue_id] - ] - ue_eff = len( - [x for x in val_ids if isinstance(x[0], float)] - ) # nombre d'étudiants avec une note dans l'UE - val_ids.sort(key=self._row_key) - ue_rangs[ue_id] = ( - comp_ranks(val_ids), - ue_eff, - ) # et non: len(self.moy_ue[ue_id]) qui est l'effectif de la promo - self.ue_rangs = ue_rangs - # ---- calcul rangs dans les modules - self.mod_rangs = {} - for modimpl in self._modimpls: - vals = self._modmoys[modimpl["moduleimpl_id"]] - val_ids = [(vals[etudid], etudid) for etudid in vals.keys()] - val_ids.sort(key=self._row_key) - self.mod_rangs[modimpl["moduleimpl_id"]] = (comp_ranks(val_ids), len(vals)) - # - self.compute_moy_moy() - # - log(f"NotesTable( formsemestre_id={formsemestre_id} ) done.") - - def _row_key(self, x): - """clé de tri par moyennes décroissantes, - en laissant les demissionnaires a la fin, par ordre alphabetique. - (moy_gen, rang_alpha) - """ - try: - moy = -float(x[0]) - except (ValueError, TypeError): - moy = 1000.0 - return (moy, self._rang_alpha[x[-1]]) - - def get_etudids(self, sorted=False): - if sorted: - # Tri par moy. generale décroissante - return [x[-1] for x in self.T] - else: - # Tri par ordre alphabetique de NOM - return [x["etudid"] for x in self.inscrlist] - - def get_sexnom(self, etudid): - "M. DUPONT" - etud = self.identdict[etudid] - return etud["civilite_str"] + " " + (etud["nom_usuel"] or etud["nom"]).upper() - - def get_nom_short(self, etudid): - "formatte nom d'un etud (pour table recap)" - etud = self.identdict[etudid] - # Attention aux caracteres multibytes pour decouper les 2 premiers: - return ( - (etud["nom_usuel"] or etud["nom"]).upper() - + " " - + etud["prenom"].capitalize()[:2] - + "." - ) - - def get_nom_long(self, etudid): - "formatte nom d'un etud: M. Pierre DUPONT" - etud = self.identdict[etudid] - return sco_etud.format_nomprenom(etud) - - def get_displayed_etud_code(self, etudid): - 'code à afficher sur les listings "anonymes"' - return self.identdict[etudid]["code_nip"] or self.identdict[etudid]["etudid"] - - def get_etud_etat(self, etudid): - "Etat de l'etudiant: 'I', 'D', DEF ou '' (si pas connu dans ce semestre)" - if etudid in self.inscrdict: - return self.inscrdict[etudid]["etat"] - else: - return "" - - def get_etud_etat_html(self, etudid): - etat = self.inscrdict[etudid]["etat"] - if etat == scu.INSCRIT: - return "" - elif etat == scu.DEMISSION: - return ' (DEMISSIONNAIRE) ' - elif etat == DEF: - return ' (DEFAILLANT) ' - else: - return ' (%s) ' % etat - - def get_ues_stat_dict(self, filter_sport=False): # was get_ues() - """Liste des UEs, ordonnée par numero. - Si filter_sport, retire les UE de type SPORT - """ - if not filter_sport: - return self._ues - else: - return [ue for ue in self._ues if ue["type"] != UE_SPORT] - - def get_modimpls_dict(self, ue_id=None): - "Liste des modules pour une UE (ou toutes si ue_id==None), triés par matières." - if ue_id is None: - r = self._modimpls - else: - r = [m for m in self._modimpls if m["ue"]["ue_id"] == ue_id] - # trie la liste par ue.numero puis mat.numero puis mod.numero - r.sort( - key=lambda x: (x["ue"]["numero"], x["mat"]["numero"], x["module"]["numero"]) - ) - return r - - def get_etud_eval_note(self, etudid, evaluation_id): - "note d'un etudiant a une evaluation" - return self._valid_evals[evaluation_id]["notes"][etudid] - - def get_evals_in_mod(self, moduleimpl_id): - "liste des evaluations valides dans un module" - return [ - e for e in self._valid_evals.values() if e["moduleimpl_id"] == moduleimpl_id - ] - - def get_mod_stats(self, moduleimpl_id): - """moyenne generale, min, max pour un module - Ne prend en compte que les evaluations où toutes les notes sont entrées - Cache le resultat. - """ - if moduleimpl_id in self.moduleimpl_stats: - return self.moduleimpl_stats[moduleimpl_id] - nb_notes = 0 - sum_notes = 0.0 - nb_missing = 0 - moys = self._modmoys[moduleimpl_id] - vals = [] - for etudid in self.get_etudids(): - # saute les demissionnaires et les défaillants: - if self.inscrdict[etudid]["etat"] != scu.INSCRIT: - continue - val = moys.get(etudid, None) # None si non inscrit - try: - vals.append(float(val)) - except: - nb_missing = nb_missing + 1 - sum_notes = sum(vals) - nb_notes = len(vals) - if nb_notes > 0: - moy = sum_notes / nb_notes - max_note, min_note = max(vals), min(vals) - else: - moy, min_note, max_note = "NA", "-", "-" - s = { - "moy": moy, - "max": max_note, - "min": min_note, - "nb_notes": nb_notes, - "nb_missing": nb_missing, - "nb_valid_evals": len(self._valid_evals_per_mod[moduleimpl_id]), - } - self.moduleimpl_stats[moduleimpl_id] = s - return s - - def compute_moy_moy(self): - """precalcule les moyennes d'UE et generale (moyennes sur tous - les etudiants), et les stocke dans self.moy_moy, self.ue['moy'] - - Les moyennes d'UE ne tiennent pas compte des capitalisations. - """ - ues = self.get_ues_stat_dict() - sum_moy = 0 # la somme des moyennes générales valides - nb_moy = 0 # le nombre de moyennes générales valides - for ue in ues: - ue["_notes"] = [] # liste tmp des valeurs de notes valides dans l'ue - nb_dem = 0 # nb d'étudiants démissionnaires dans le semestre - nb_def = 0 # nb d'étudiants défaillants dans le semestre - T = self.get_table_moyennes_triees() - for t in T: - etudid = t[-1] - # saute les demissionnaires et les défaillants: - if self.inscrdict[etudid]["etat"] != scu.INSCRIT: - if self.inscrdict[etudid]["etat"] == scu.DEMISSION: - nb_dem += 1 - if self.inscrdict[etudid]["etat"] == DEF: - nb_def += 1 - continue - try: - sum_moy += float(t[0]) - nb_moy += 1 - except: - pass - i = 0 - for ue in ues: - i += 1 - try: - ue["_notes"].append(float(t[i])) - except: - pass - self.nb_demissions = nb_dem - self.nb_defaillants = nb_def - if nb_moy > 0: - self.moy_moy = sum_moy / nb_moy - else: - self.moy_moy = "-" - - i = 0 - for ue in ues: - i += 1 - ue["nb_vals"] = len(ue["_notes"]) - if ue["nb_vals"] > 0: - ue["moy"] = sum(ue["_notes"]) / ue["nb_vals"] - ue["max"] = max(ue["_notes"]) - ue["min"] = min(ue["_notes"]) - else: - ue["moy"], ue["max"], ue["min"] = "", "", "" - del ue["_notes"] - - def get_etud_mod_moy(self, moduleimpl_id, etudid): - """moyenne d'un etudiant dans un module (ou NI si non inscrit)""" - return self._modmoys[moduleimpl_id].get(etudid, "NI") - - def get_etud_mat_moy(self, matiere_id, etudid): - """moyenne d'un étudiant dans une matière (ou NA si pas de notes)""" - matmoy = self._matmoys.get(matiere_id, None) - if not matmoy: - return "NM" # non inscrit - # log('*** oups: get_etud_mat_moy(%s, %s)' % (matiere_id, etudid)) - # raise ValueError('matiere invalide !') # should not occur - return matmoy.get(etudid, "NA") - - def comp_etud_moy_ue(self, etudid, ue_id=None, cnx=None): - """Calcule moyenne gen. pour un etudiant dans une UE - Ne prend en compte que les evaluations où toutes les notes sont entrées - Return a dict(moy, nb_notes, nb_missing, sum_coefs) - Si pas de notes, moy == 'NA' et sum_coefs==0 - Si non inscrit, moy == 'NI' et sum_coefs==0 - """ - assert ue_id - modimpls = self.get_modimpls_dict(ue_id) - nb_notes = 0 # dans cette UE - sum_notes = 0.0 - sum_coefs = 0.0 - nb_missing = 0 # nb de modules sans note dans cette UE - - notes_bonus_gen = [] # liste des notes de sport et culture - coefs_bonus_gen = [] - - ue_malus = 0.0 # malus à appliquer à cette moyenne d'UE - - notes = NoteVector() - coefs = NoteVector() - coefs_mask = NoteVector() # 0/1, 0 si coef a ete annulé - - matiere_id_last = None - matiere_sum_notes = matiere_sum_coefs = 0.0 - - est_inscrit = False # inscrit à l'un des modules de cette UE ? - - for modimpl in modimpls: - # module ne faisant pas partie d'une UE capitalisee - val = self._modmoys[modimpl["moduleimpl_id"]].get(etudid, "NI") - # si 'NI', etudiant non inscrit a ce module - if val != "NI": - est_inscrit = True - if modimpl["module"]["module_type"] == ModuleType.STANDARD: - coef = modimpl["module"]["coefficient"] - if modimpl["ue"]["type"] != UE_SPORT: - notes.append(val, name=modimpl["module"]["code"]) - try: - sum_notes += val * coef - sum_coefs += coef - nb_notes = nb_notes + 1 - coefs.append(coef) - coefs_mask.append(1) - matiere_id = modimpl["module"]["matiere_id"] - if ( - matiere_id_last - and matiere_id != matiere_id_last - and matiere_sum_coefs - ): - self._matmoys[matiere_id_last][etudid] = ( - matiere_sum_notes / matiere_sum_coefs - ) - matiere_sum_notes = matiere_sum_coefs = 0.0 - matiere_sum_notes += val * coef - matiere_sum_coefs += coef - matiere_id_last = matiere_id - except TypeError: # val == "NI" "NA" - assert val == "NI" or val == "NA" or val == "ERR" - nb_missing = nb_missing + 1 - coefs.append(0) - coefs_mask.append(0) - - else: # UE_SPORT: - # la note du module de sport agit directement sur la moyenne gen. - try: - notes_bonus_gen.append(float(val)) - coefs_bonus_gen.append(coef) - except: - # log('comp_etud_moy_ue: exception: val=%s coef=%s' % (val,coef)) - pass - elif modimpl["module"]["module_type"] == ModuleType.MALUS: - try: - ue_malus += val - except: - pass # si non inscrit ou manquant, ignore - elif modimpl["module"]["module_type"] in ( - ModuleType.RESSOURCE, - ModuleType.SAE, - ): - # XXX temporaire pour ne pas bloquer durant le dev - pass - else: - raise ValueError( - "invalid module type (%s)" % modimpl["module"]["module_type"] - ) - - if matiere_id_last and matiere_sum_coefs: - self._matmoys[matiere_id_last][etudid] = ( - matiere_sum_notes / matiere_sum_coefs - ) - - # Calcul moyenne: - if sum_coefs > 0: - moy = sum_notes / sum_coefs - if ue_malus: - moy -= ue_malus - moy = max(scu.NOTES_MIN, min(moy, 20.0)) - moy_valid = True - else: - moy = "NA" - moy_valid = False - - # Recalcule la moyenne en utilisant une formule utilisateur - expr_diag = {} - formula = sco_compute_moy.get_ue_expression(self.formsemestre_id, ue_id) - if formula: - moy = sco_compute_moy.compute_user_formula( - self.sem, - etudid, - moy, - moy_valid, - notes, - coefs, - coefs_mask, - formula, - diag_info=expr_diag, - ) - if expr_diag: - expr_diag["ue_id"] = ue_id - self.expr_diagnostics.append(expr_diag) - - return dict( - moy=moy, - nb_notes=nb_notes, - nb_missing=nb_missing, - sum_coefs=sum_coefs, - notes_bonus_gen=notes_bonus_gen, - coefs_bonus_gen=coefs_bonus_gen, - expr_diag=expr_diag, - ue_malus=ue_malus, - est_inscrit=est_inscrit, - ) - - def comp_etud_moy_gen(self, etudid, cnx): - """Calcule moyenne gen. pour un etudiant - Return a dict: - moy : moyenne générale - nb_notes, nb_missing, sum_coefs - ects_pot : (float) nb de crédits ECTS qui seraient validés (sous réserve de validation par le jury), - ects_pot_fond: (float) nb d'ECTS issus d'UE fondamentales (non électives) - ects_pot_pro: (float) nb d'ECTS issus d'UE pro - moy_ues : { ue_id : ue_status } - où ue_status = { - 'est_inscrit' : True si étudiant inscrit à au moins un module de cette UE - 'moy' : moyenne, avec capitalisation eventuelle - 'capitalized_ue_id' : id de l'UE capitalisée - 'coef_ue' : coef de l'UE utilisé pour le calcul de la moyenne générale - (la somme des coefs des modules, ou le coef d'UE capitalisée, - ou encore le coef d'UE si l'option use_ue_coefs est active) - 'cur_moy_ue' : moyenne de l'UE en cours (sans considérer de capitalisation) - 'cur_coef_ue': coefficient de l'UE courante (inutilisé ?) - 'is_capitalized' : True|False, - 'ects_pot' : (float) nb de crédits ECTS qui seraient validés (sous réserve de validation par le jury), - 'ects_pot_fond': 0. si UE non fondamentale, = ects_pot sinon, - 'ects_pot_pro' : 0 si UE non pro, = ects_pot sinon, - 'formsemestre_id' : (si capitalisee), - 'event_date' : (si capitalisee) - } - Si pas de notes, moy == 'NA' et sum_coefs==0 - - Prend toujours en compte les UE capitalisées. - """ - # Si l'étudiant a Démissionné ou est DEFaillant, on n'enregistre pas ses moyennes - block_computation = ( - self.inscrdict[etudid]["etat"] == "D" - or self.inscrdict[etudid]["etat"] == DEF - or self.block_moyennes - ) - - moy_ues = {} - notes_bonus_gen = ( - [] - ) # liste des notes de sport et culture (s'appliquant à la MG) - coefs_bonus_gen = [] - nb_notes = 0 # nb de notes d'UE (non capitalisees) - sum_notes = 0.0 # somme des notes d'UE - # somme des coefs d'UE (eux-même somme des coefs de modules avec notes): - sum_coefs = 0.0 - - nb_missing = 0 # nombre d'UE sans notes - sem_ects_pot = 0.0 - sem_ects_pot_fond = 0.0 - sem_ects_pot_pro = 0.0 - - for ue in self.get_ues_stat_dict(): - # - On calcule la moyenne d'UE courante: - if not block_computation: - mu = self.comp_etud_moy_ue(etudid, ue_id=ue["ue_id"], cnx=cnx) - else: - mu = dict( - moy="NA", - nb_notes=0, - nb_missing=0, - sum_coefs=0, - notes_bonus_gen=0, - coefs_bonus_gen=0, - expr_diag="", - est_inscrit=False, - ) - # infos supplementaires pouvant servir au calcul du bonus sport - mu["ue"] = ue - moy_ues[ue["ue_id"]] = mu - - # - Faut-il prendre une UE capitalisée ? - if mu["moy"] != "NA" and mu["est_inscrit"]: - max_moy_ue = mu["moy"] - else: - # pas de notes dans l'UE courante, ou pas inscrit - max_moy_ue = 0.0 - if not mu["est_inscrit"]: - coef_ue = 0.0 - else: - if self.use_ue_coefs: - coef_ue = mu["ue"]["coefficient"] - else: - # coef UE = sum des coefs modules - coef_ue = mu["sum_coefs"] - - # is_capitalized si l'UE prise en compte est une UE capitalisée - mu["is_capitalized"] = False - # was_capitalized s'il y a precedemment une UE capitalisée (pas forcement meilleure) - mu["was_capitalized"] = False - - is_external = False - event_date = None - if not block_computation: - for ue_cap in self.ue_capitalisees[etudid]: - if ue_cap["ue_code"] == ue["ue_code"]: - moy_ue_cap = ue_cap["moy"] - mu["was_capitalized"] = True - event_date = event_date or ue_cap["event_date"] - if ( - (moy_ue_cap != "NA") - and isinstance(moy_ue_cap, float) - and isinstance(max_moy_ue, float) - and (moy_ue_cap > max_moy_ue) - ): - # meilleure UE capitalisée - event_date = ue_cap["event_date"] - max_moy_ue = moy_ue_cap - mu["is_capitalized"] = True - capitalized_ue_id = ue_cap["ue_id"] - formsemestre_id = ue_cap["formsemestre_id"] - coef_ue = self.get_etud_ue_cap_coef( - etudid, ue, ue_cap, cnx=cnx - ) - is_external = ue_cap["is_external"] - - mu["cur_moy_ue"] = mu["moy"] # la moyenne dans le sem. courant - if mu["est_inscrit"]: - mu["cur_coef_ue"] = mu["sum_coefs"] - else: - mu["cur_coef_ue"] = 0.0 - mu["moy"] = max_moy_ue # la moyenne d'UE a prendre en compte - mu["is_external"] = is_external # validation externe (dite "antérieure") - mu["coef_ue"] = coef_ue # coef reel ou coef de l'ue si capitalisee - - if mu["is_capitalized"]: - mu["formsemestre_id"] = formsemestre_id - mu["capitalized_ue_id"] = capitalized_ue_id - if mu["was_capitalized"]: - mu["event_date"] = event_date - # - ECTS ? ("pot" pour "potentiels" car les ECTS ne seront acquises qu'apres validation du jury - if ( - isinstance(mu["moy"], float) - and mu["moy"] >= self.parcours.NOTES_BARRE_VALID_UE - ): - mu["ects_pot"] = ue["ects"] or 0.0 - if ue_is_fondamentale(ue["type"]): - mu["ects_pot_fond"] = mu["ects_pot"] - else: - mu["ects_pot_fond"] = 0.0 - if ue_is_professionnelle(ue["type"]): - mu["ects_pot_pro"] = mu["ects_pot"] - else: - mu["ects_pot_pro"] = 0.0 - else: - mu["ects_pot"] = 0.0 - mu["ects_pot_fond"] = 0.0 - mu["ects_pot_pro"] = 0.0 - sem_ects_pot += mu["ects_pot"] - sem_ects_pot_fond += mu["ects_pot_fond"] - sem_ects_pot_pro += mu["ects_pot_pro"] - - # - Calcul moyenne générale dans le semestre: - if mu["is_capitalized"]: - try: - sum_notes += mu["moy"] * mu["coef_ue"] - sum_coefs += mu["coef_ue"] - except: # pas de note dans cette UE - pass - else: - if mu["coefs_bonus_gen"]: - notes_bonus_gen.extend(mu["notes_bonus_gen"]) - coefs_bonus_gen.extend(mu["coefs_bonus_gen"]) - # - try: - sum_notes += mu["moy"] * mu["sum_coefs"] - sum_coefs += mu["sum_coefs"] - nb_notes = nb_notes + 1 - except TypeError: - nb_missing = nb_missing + 1 - # Le resultat: - infos = dict( - nb_notes=nb_notes, - nb_missing=nb_missing, - sum_coefs=sum_coefs, - moy_ues=moy_ues, - ects_pot=sem_ects_pot, - ects_pot_fond=sem_ects_pot_fond, - ects_pot_pro=sem_ects_pot_pro, - sem=self.sem, - ) - # ---- Calcul moyenne (avec bonus sport&culture) - if sum_coefs <= 0 or block_computation: - infos["moy"] = "NA" - else: - if self.use_ue_coefs: - # Calcul optionnel (mai 2020) - # moyenne pondére par leurs coefficients des moyennes d'UE - sum_moy_ue = 0 - sum_coefs_ue = 0 - for mu in moy_ues.values(): - # mu["moy"] can be a number, or "NA", or "ERR" (user-defined UE formulas) - if ( - (mu["ue"]["type"] != UE_SPORT) - and scu.isnumber(mu["moy"]) - and (mu["est_inscrit"] or mu["is_capitalized"]) - ): - coef_ue = mu["ue"]["coefficient"] - sum_moy_ue += mu["moy"] * coef_ue - sum_coefs_ue += coef_ue - if sum_coefs_ue != 0: - infos["moy"] = sum_moy_ue / sum_coefs_ue - else: - infos["moy"] = "NA" - else: - # Calcul standard ScoDoc: moyenne pondérée des notes de modules - infos["moy"] = sum_notes / sum_coefs - - if notes_bonus_gen and infos["moy"] != "NA": - # regle de calcul maison (configurable, voir bonus_sport.py) - if sum(coefs_bonus_gen) <= 0 and len(coefs_bonus_gen) != 1: - log( - "comp_etud_moy_gen: invalid or null coefficient (%s) for notes_bonus_gen=%s (etudid=%s, formsemestre_id=%s)" - % ( - coefs_bonus_gen, - notes_bonus_gen, - etudid, - self.formsemestre_id, - ) - ) - bonus = 0 - else: - if len(coefs_bonus_gen) == 1: - coefs_bonus_gen = [1.0] # irrelevant, may be zero - - # XXX attention: utilise anciens bonus_sport, évidemment - bonus_func = ScoDocSiteConfig.get_bonus_sport_func() - if bonus_func: - bonus = bonus_func( - notes_bonus_gen, coefs_bonus_gen, infos=infos - ) - else: - bonus = 0.0 - self.bonus[etudid] = bonus - infos["moy"] += bonus - infos["moy"] = min(infos["moy"], 20.0) # clip bogus bonus - - return infos - - def get_etud_moy_gen(self, etudid): # -> float | str - """Moyenne generale de cet etudiant dans ce semestre. - Prend en compte les UE capitalisées. - Si pas de notes: 'NA' - """ - return self.moy_gen[etudid] - - def get_etud_moy_infos(self, etudid): # XXX OBSOLETE - """Infos sur moyennes""" - return self.etud_moy_infos[etudid] - - # was etud_has_all_ue_over_threshold: - def etud_check_conditions_ues(self, etudid): - """Vrai si les conditions sur les UE sont remplies. - Ne considère que les UE ayant des notes (moyenne calculée). - (les UE sans notes ne sont pas comptées comme sous la barre) - Prend en compte les éventuelles UE capitalisées. - - Pour les parcours habituels, cela revient à vérifier que - les moyennes d'UE sont toutes > à leur barre (sauf celles sans notes) - - Pour les parcours non standards (LP2014), cela peut être plus compliqué. - - Return: True|False, message explicatif - """ - ue_status_list = [] - for ue in self._ues: - ue_status = self.get_etud_ue_status(etudid, ue["ue_id"]) - if ue_status: - ue_status_list.append(ue_status) - return self.parcours.check_barre_ues(ue_status_list) - - def get_table_moyennes_triees(self): - return self.T - - def get_etud_rang(self, etudid) -> str: - return self.etud_moy_gen_ranks.get(etudid, "999") - - def get_etud_rang_group(self, etudid, group_id): - """Returns rank of etud in this group and number of etuds in group. - If etud not in group, returns None. - """ - if not group_id in self.rangs_groupes: - # lazy: fill rangs_groupes on demand - # { groupe : { etudid : rang } } - if not group_id in self.group_etuds: - # lazy fill: list of etud in group_id - etuds = sco_groups.get_group_members(group_id) - self.group_etuds[group_id] = set([x["etudid"] for x in etuds]) - # 1- build T restricted to group - Tr = [] - for t in self.get_table_moyennes_triees(): - t_etudid = t[-1] - if t_etudid in self.group_etuds[group_id]: - Tr.append(t) - # - self.rangs_groupes[group_id] = comp_ranks(Tr) - - return ( - self.rangs_groupes[group_id].get(etudid, None), - len(self.rangs_groupes[group_id]), - ) - - def get_table_moyennes_dict(self): - """{ etudid : (liste des moyennes) } comme get_table_moyennes_triees""" - D = {} - for t in self.T: - D[t[-1]] = t - return D - - def get_moduleimpls_attente(self): - "Liste des moduleimpls avec des notes en attente" - return self._mods_att - - # Decisions existantes du jury - def comp_decisions_jury(self): - """Cherche les decisions du jury pour le semestre (pas les UE). - Calcule l'attribut: - decisions_jury = { etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }} - decision_jury_ues={ etudid : { ue_id : { 'code' : Note|ADM|CMP, 'event_date' }}} - Si la decision n'a pas été prise, la clé etudid n'est pas présente. - Si l'étudiant est défaillant, met un code DEF sur toutes les UE - """ - cnx = ndb.GetDBConnexion() - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) - cursor.execute( - """SELECT etudid, code, assidu, compense_formsemestre_id, event_date - FROM scolar_formsemestre_validation - WHERE formsemestre_id=%(formsemestre_id)s AND ue_id is NULL; - """, - {"formsemestre_id": self.formsemestre_id}, - ) - decisions_jury = {} - for ( - etudid, - code, - assidu, - compense_formsemestre_id, - event_date, - ) in cursor.fetchall(): - decisions_jury[etudid] = { - "code": code, - "assidu": assidu, - "compense_formsemestre_id": compense_formsemestre_id, - "event_date": ndb.DateISOtoDMY(event_date), - } - - self.decisions_jury = decisions_jury - # UEs: - cursor.execute( - "select etudid, ue_id, code, event_date from scolar_formsemestre_validation where formsemestre_id=%(formsemestre_id)s and ue_id is not NULL;", - {"formsemestre_id": self.formsemestre_id}, - ) - decisions_jury_ues = {} - for (etudid, ue_id, code, event_date) in cursor.fetchall(): - if etudid not in decisions_jury_ues: - decisions_jury_ues[etudid] = {} - # Calcul des ECTS associes a cette UE: - ects = 0.0 - if codes_cursus.code_ue_validant(code): - ue = self.uedict.get(ue_id, None) - if ue is None: # not in list for this sem ??? (probably an error) - log( - "Warning: %s capitalized an UE %s which is not part of current sem %s" - % (etudid, ue_id, self.formsemestre_id) - ) - ue = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0] - self.uedict[ue_id] = ue # record this UE - if ue_id not in self._uecoef: - cl = formsemestre_uecoef_list( - cnx, - args={ - "formsemestre_id": self.formsemestre_id, - "ue_id": ue_id, - }, - ) - if not cl: - # cas anormal: UE capitalisee, pas dans ce semestre, et sans coef - log("Warning: setting UE coef to zero") - formsemestre_uecoef_create( - cnx, - args={ - "formsemestre_id": self.formsemestre_id, - "ue_id": ue_id, - "coefficient": 0, - }, - ) - - ects = ue["ects"] or 0.0 # 0 if None - - decisions_jury_ues[etudid][ue_id] = { - "code": code, - "ects": ects, # 0. si non UE validée ou si mode de calcul different (?) - "event_date": ndb.DateISOtoDMY(event_date), - } - - self.decisions_jury_ues = decisions_jury_ues - - def get_etud_decision_sem(self, etudid): - """Decision du jury prise pour cet etudiant, ou None s'il n'y en pas eu. - { 'code' : None|ATT|..., 'assidu' : 0|1, 'event_date' : , compense_formsemestre_id } - Si état défaillant, force le code a DEF - """ - if self.get_etud_etat(etudid) == DEF: - return { - "code": DEF, - "assidu": False, - "event_date": "", - "compense_formsemestre_id": None, - } - else: - return self.decisions_jury.get(etudid, None) - - def get_etud_decision_ues(self, etudid): - """Decisions du jury pour les UE de cet etudiant, ou None s'il n'y en pas eu. - Ne tient pas compte des UE capitalisées. - { ue_id : { 'code' : ADM|CMP|AJ, 'event_date' : } - Ne renvoie aucune decision d'UE pour les défaillants - """ - if self.get_etud_etat(etudid) == DEF: - return {} - else: - return self.decisions_jury_ues.get(etudid, None) - - def sem_has_decisions(self): - """True si au moins une decision de jury dans ce semestre""" - if [x for x in self.decisions_jury_ues.values() if x]: - return True - - return len([x for x in self.decisions_jury_ues.values() if x]) > 0 - - def etud_has_decision(self, etudid): - """True s'il y a une décision de jury pour cet étudiant""" - return self.get_etud_decision_ues(etudid) or self.get_etud_decision_sem(etudid) - - def all_etuds_have_sem_decisions(self): - """True si tous les étudiants du semestre ont une décision de jury. - ne regarde pas les décisions d'UE (todo: à voir ?) - """ - for etudid in self.get_etudids(): - if self.inscrdict[etudid]["etat"] == "D": - continue # skip demissionnaires - if self.get_etud_decision_sem(etudid) is None: - return False - return True - - # Capitalisation des UEs - def comp_ue_capitalisees(self): - """Cherche pour chaque etudiant ses UE capitalisées dans ce semestre. - Calcule l'attribut: - ue_capitalisees = { etudid : - [{ 'moy':, 'event_date' : ,'formsemestre_id' : }, ...] } - """ - self.ue_capitalisees = scu.DictDefault(defaultvalue=[]) - cnx = None - semestre_id = self.sem["semestre_id"] - for etudid in self.get_etudids(): - capital = formsemestre_get_etud_capitalisation( - self.formation["id"], - semestre_id, - ndb.DateDMYtoISO(self.sem["date_debut"]), - etudid, - ) - for ue_cap in capital: - # Si la moyenne d'UE n'avait pas été stockée (anciennes versions de ScoDoc) - # il faut la calculer ici et l'enregistrer - if ue_cap["moy_ue"] is None: - log( - "comp_ue_capitalisees: recomputing UE moy (etudid=%s, ue_id=%s formsemestre_id=%s)" - % (etudid, ue_cap["ue_id"], ue_cap["formsemestre_id"]) - ) - nt_cap = sco_cache.NotesTableCache.get( - ue_cap["formsemestre_id"] - ) # > UE capitalisees par un etud - ue_cap_status = nt_cap.get_etud_ue_status(etudid, ue_cap["ue_id"]) - if ue_cap_status: - moy_ue_cap = ue_cap_status["moy"] - else: - moy_ue_cap = "" - ue_cap["moy_ue"] = moy_ue_cap - if ( - isinstance(moy_ue_cap, float) - and moy_ue_cap >= self.parcours.NOTES_BARRE_VALID_UE - ): - if not cnx: - cnx = ndb.GetDBConnexion() - sco_cursus_dut.do_formsemestre_validate_ue( - cnx, - nt_cap, - ue_cap["formsemestre_id"], - etudid, - ue_cap["ue_id"], - ue_cap["code"], - ) - else: - log( - "*** valid inconsistency: moy_ue_cap=%s (etudid=%s, ue_id=%s formsemestre_id=%s)" - % ( - moy_ue_cap, - etudid, - ue_cap["ue_id"], - ue_cap["formsemestre_id"], - ) - ) - ue_cap["moy"] = ue_cap["moy_ue"] # backward compat (needs refactoring) - self.ue_capitalisees[etudid].append(ue_cap) - if cnx: - cnx.commit() - # log('comp_ue_capitalisees=\n%s' % pprint.pformat(self.ue_capitalisees) ) - - # def comp_etud_sum_coef_modules_ue( etudid, ue_id): - # """Somme des coefficients des modules de l'UE dans lesquels cet étudiant est inscrit - # ou None s'il n'y a aucun module - # """ - # c_list = [ mod['module']['coefficient'] - # for mod in self._modimpls - # if (( mod['module']['ue_id'] == ue_id) - # and self._modmoys[mod['moduleimpl_id']].get(etudid, False) is not False) - # ] - # if not c_list: - # return None - # return sum(c_list) - - def get_etud_ue_cap_coef(self, etudid, ue, ue_cap, cnx=None): - """Calcule le coefficient d'une UE capitalisée, pour cet étudiant, - injectée dans le semestre courant. - - ue : ue du semestre courant - - ue_cap = resultat de formsemestre_get_etud_capitalisation - { 'ue_id' (dans le semestre source), - 'ue_code', 'moy', 'event_date','formsemestre_id' } - """ - # log("get_etud_ue_cap_coef\nformsemestre_id='%s'\netudid='%s'\nue=%s\nue_cap=%s\n" % (self.formsemestre_id, etudid, ue, ue_cap)) - # 1- Coefficient explicitement déclaré dans le semestre courant pour cette UE ? - if ue["ue_id"] not in self._uecoef: - self._uecoef[ue["ue_id"]] = formsemestre_uecoef_list( - cnx, - args={"formsemestre_id": self.formsemestre_id, "ue_id": ue["ue_id"]}, - ) - - if len(self._uecoef[ue["ue_id"]]): - # utilisation du coef manuel - return self._uecoef[ue["ue_id"]][0]["coefficient"] - - # 2- Mode automatique: calcul du coefficient - # Capitalisation depuis un autre semestre ScoDoc ? - coef = None - if ue_cap["formsemestre_id"]: - # Somme des coefs dans l'UE du semestre d'origine (nouveau: 23/01/2016) - coef = comp_etud_sum_coef_modules_ue( - ue_cap["formsemestre_id"], etudid, ue_cap["ue_id"] - ) - if coef != None: - return coef - else: - # Capitalisation UE externe: quel coef appliquer ? - # Si l'étudiant est inscrit dans le semestre courant, - # somme des coefs des modules de l'UE auxquels il est inscrit - c = comp_etud_sum_coef_modules_ue(self.formsemestre_id, etudid, ue["ue_id"]) - if c is not None: # inscrit à au moins un module de cette UE - return c - # arfff: aucun moyen de déterminer le coefficient de façon sûre - log( - "* oups: calcul coef UE impossible\nformsemestre_id='%s'\netudid='%s'\nue=%s\nue_cap=%s" - % (self.formsemestre_id, etudid, ue, ue_cap) - ) - raise ScoValueError( - """

Coefficient de l'UE capitalisée %s impossible à déterminer - pour l'étudiant %s

-

Il faut saisir le coefficient de cette UE avant de continuer

-
- """ - % ( - ue["acronyme"], - url_for( - "scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid - ), - self.get_nom_long(etudid), - url_for( - "notes.formsemestre_edit_uecoefs", - scodoc_dept=g.scodoc_dept, - formsemestre_id=self.formsemestre_id, - err_ue_id=ue["ue_id"], - ), - ) - ) - - return 0.0 # ? - - def get_etud_ue_status(self, etudid, ue_id): - "Etat de cette UE (note, coef, capitalisation, ...)" - return self._etud_moy_ues[etudid][ue_id] - - def etud_has_notes_attente(self, etudid): - """Vrai si cet etudiant a au moins une note en attente dans ce semestre. - (ne compte que les notes en attente dans des évaluation avec coef. non nul). - """ - cnx = ndb.GetDBConnexion() - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) - cursor.execute( - """SELECT n.* - FROM notes_notes n, notes_evaluation e, notes_moduleimpl m, - notes_moduleimpl_inscription i - WHERE n.etudid = %(etudid)s - and n.value = %(code_attente)s - and n.evaluation_id = e.id - and e.moduleimpl_id = m.id - and m.formsemestre_id = %(formsemestre_id)s - and e.coefficient != 0 - and m.id = i.moduleimpl_id - and i.etudid=%(etudid)s - """, - { - "formsemestre_id": self.formsemestre_id, - "etudid": etudid, - "code_attente": scu.NOTES_ATTENTE, - }, - ) - return len(cursor.fetchall()) > 0 - - def get_evaluations_etats(self): # evaluation_list_in_sem - """[ {...evaluation et son etat...} ]""" - if self._evaluations_etats is None: - self._evaluations_etats = sco_evaluations.do_evaluation_list_in_sem( - self.formsemestre_id - ) - - return self._evaluations_etats - - def get_mod_evaluation_etat_list(self, moduleimpl_id) -> list[dict]: - """Liste des évaluations de ce module""" - return [ - e - for e in self.get_evaluations_etats() - if e["moduleimpl_id"] == moduleimpl_id - ] - - def apc_recompute_moyennes(self): - """recalcule les moyennes en APC (BUT) - et modifie en place le tableau T. - XXX Raccord provisoire avant refonte de cette classe. - """ - assert self.parcours.APC_SAE - formsemestre = FormSemestre.query.get(self.formsemestre_id) - results = bulletin_but.ResultatsSemestreBUT(formsemestre) - - # Rappel des épisodes précédents: T est une liste de liste - # Colonnes: 0 moy_gen, moy_ue1, ..., moy_ue_n, moy_mod1, ..., moy_mod_n, etudid - ues = self.get_ues_stat_dict() # incluant le(s) UE de sport - for t in self.T: - etudid = t[-1] - if etudid in results.etud_moy_gen: # evite les démissionnaires - t[0] = results.etud_moy_gen[etudid] - for i, ue in enumerate(ues, start=1): - if ue["type"] != UE_SPORT: - # temporaire pour 9.1.29 ! - if ue["id"] in results.etud_moy_ue: - t[i] = results.etud_moy_ue[ue["id"]][etudid] - else: - t[i] = "" - # re-trie selon la nouvelle moyenne générale: - self.T.sort(key=self._row_key) - # Remplace aussi le rang: - self.etud_moy_gen_ranks = results.etud_moy_gen_ranks diff --git a/app/scodoc/sco_archives.py b/app/scodoc/sco_archives.py index 7991a948..3b50c4e6 100644 --- a/app/scodoc/sco_archives.py +++ b/app/scodoc/sco_archives.py @@ -76,9 +76,9 @@ from app.scodoc import html_sco_header from app.scodoc import sco_bulletins_pdf from app.scodoc import sco_groups from app.scodoc import sco_groups_view -from app.scodoc import sco_pvjury -from app.scodoc import sco_dict_pv_jury -from app.scodoc import sco_pvpdf +from app.scodoc import sco_pv_forms +from app.scodoc import sco_pv_lettres_inviduelles +from app.scodoc import sco_pv_pdf from app.scodoc.sco_exceptions import ScoValueError @@ -398,27 +398,23 @@ def do_formsemestre_archive( signature=signature, ) if data: - PVArchive.store(archive_id, "CourriersDecisions%s.pdf" % groups_filename, data) + PVArchive.store(archive_id, f"CourriersDecisions{groups_filename}.pdf", data) - # PV de jury (PDF): disponible seulement en classique - # en BUT, le PV est sous forme excel (Decisions_Jury.xlsx ci-dessus) - if not formsemestre.formation.is_apc(): - dpv = sco_dict_pv_jury.dict_pvjury( - formsemestre_id, etudids=etudids, with_prev=True - ) - data = sco_pvpdf.pvjury_pdf( - dpv, - date_commission=date_commission, - date_jury=date_jury, - numero_arrete=numero_arrete, - code_vdi=code_vdi, - show_title=show_title, - pv_title=pv_title, - with_paragraph_nom=with_paragraph_nom, - anonymous=anonymous, - ) - if data: - PVArchive.store(archive_id, "PV_Jury%s.pdf" % groups_filename, data) + # PV de jury (PDF): + data = sco_pv_pdf.pvjury_pdf( + formsemestre, + etudids=etudids, + date_commission=date_commission, + date_jury=date_jury, + numero_arrete=numero_arrete, + code_vdi=code_vdi, + show_title=show_title, + pv_title=pv_title, + with_paragraph_nom=with_paragraph_nom, + anonymous=anonymous, + ) + if data: + PVArchive.store(archive_id, f"PV_Jury{groups_filename}.pdf", data) def formsemestre_archive(formsemestre_id, group_ids: list[int] = None): @@ -474,7 +470,7 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement. ), ("sep", {"input_type": "separator", "title": "Informations sur PV de jury"}), ] - descr += sco_pvjury.descrform_pvjury(formsemestre) + descr += sco_pv_forms.descrform_pvjury(formsemestre) descr += [ ( "signature", diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index 62f4cdf1..82191269 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -67,7 +67,7 @@ from app.scodoc import sco_evaluation_db from app.scodoc import sco_formsemestre from app.scodoc import sco_groups from app.scodoc import sco_preferences -from app.scodoc import sco_dict_pv_jury +from app.scodoc import sco_pv_dict from app.scodoc import sco_users import app.scodoc.sco_utils as scu from app.scodoc.sco_utils import ModuleType, fmt_note diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index 14a3b40c..623753dd 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -49,7 +49,6 @@ from app.models import ( ) from app.models import ApcValidationRCUE, ScolarFormSemestreValidation, ScolarEvent from app.models import ScolarNews -from app.models.formations import Matiere import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu from app.scodoc.sco_utils import ModuleType @@ -66,7 +65,6 @@ from app.scodoc import codes_cursus from app.scodoc import sco_edit_apc from app.scodoc import sco_edit_matiere from app.scodoc import sco_edit_module -from app.scodoc import sco_formsemestre from app.scodoc import sco_groups from app.scodoc import sco_moduleimpl from app.scodoc import sco_tag_module @@ -844,7 +842,8 @@ du programme" (menu "Semestre") si vous avez un semestre en cours); H.append( f"""
  • {'Visualiser' if locked else 'Éditer'} les coefficients des ressources et SAÉs
  • diff --git a/app/scodoc/sco_evaluation_db.py b/app/scodoc/sco_evaluation_db.py index 134bcd1a..b99af2cc 100644 --- a/app/scodoc/sco_evaluation_db.py +++ b/app/scodoc/sco_evaluation_db.py @@ -36,7 +36,7 @@ from flask_login import current_user from app import db, log -from app.models import ModuleImpl, ScolarNews +from app.models import Evaluation, ModuleImpl, ScolarNews from app.models.evaluations import evaluation_enrich_dict, check_evaluation_args import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb diff --git a/app/scodoc/sco_export_results.py b/app/scodoc/sco_export_results.py index 209fe987..01c6c536 100644 --- a/app/scodoc/sco_export_results.py +++ b/app/scodoc/sco_export_results.py @@ -40,7 +40,7 @@ from app.scodoc import html_sco_header from app.scodoc import sco_bac from app.scodoc import codes_cursus from app.scodoc import sco_preferences -from app.scodoc import sco_dict_pv_jury +from app.scodoc import sco_pv_dict from app.scodoc import sco_etud import sco_version from app.scodoc.gen_tables import GenTable @@ -57,16 +57,14 @@ def _build_results_table(start_date=None, end_date=None, types_parcours=[]): # Décisions de jury de tous les semestres: dpv_by_sem = {} for formsemestre_id in formsemestre_ids: - dpv_by_sem[formsemestre_id] = sco_dict_pv_jury.dict_pvjury( + dpv_by_sem[formsemestre_id] = sco_pv_dict.dict_pvjury( formsemestre_id, with_parcours_decisions=True ) semlist = [dpv["formsemestre"] for dpv in dpv_by_sem.values() if dpv] semlist_parcours = [] for sem in semlist: - sem["formation"] = sco_formations.formation_list( - args={"formation_id": sem["formation_id"]} - )[0] + sem["formation"] = Formation.query.get_or_404(sem["formation_id"]).to_dict() sem["parcours"] = codes_cursus.get_cursus_from_code( sem["formation"]["type_parcours"] ) @@ -350,7 +348,7 @@ end_date='2017-08-31' formsemestre_ids = get_set_formsemestre_id_dates( start_date, end_date) dpv_by_sem = {} for formsemestre_id in formsemestre_ids: - dpv_by_sem[formsemestre_id] = sco_dict_pv_jury.dict_pvjury( formsemestre_id, with_parcours_decisions=True) + dpv_by_sem[formsemestre_id] = sco_pv_dict.dict_pvjury( formsemestre_id, with_parcours_decisions=True) semlist = [ dpv['formsemestre'] for dpv in dpv_by_sem.values() ] diff --git a/app/scodoc/sco_formations.py b/app/scodoc/sco_formations.py index 452b423b..be7e348c 100644 --- a/app/scodoc/sco_formations.py +++ b/app/scodoc/sco_formations.py @@ -482,20 +482,24 @@ def formation_list_table() -> GenTable: editable = current_user.has_permission(Permission.ScoChangeFormation) # Traduit/ajoute des champs à afficher: - for f in formations: - try: - f["parcours_name"] = codes_cursus.get_cursus_from_code( - f["type_parcours"] - ).NAME - except: - f["parcours_name"] = "" - f["_titre_target"] = url_for( - "notes.ue_table", - scodoc_dept=g.scodoc_dept, - formation_id=str(f["formation_id"]), - ) - f["_titre_link_class"] = "stdlink" - f["_titre_id"] = "titre-%s" % f["acronyme"].lower().replace(" ", "-") + rows = [] + for formation in formations: + acronyme_no_spaces = formation.acronyme.lower().replace(" ", "-") + row = { + "acronyme": formation.acronyme, + "parcours_name": codes_cursus.get_cursus_from_code( + formation.type_parcours + ).NAME, + "titre": formation.titre, + "_titre_target": url_for( + "notes.ue_table", + scodoc_dept=g.scodoc_dept, + formation_id=formation.id, + ), + "_titre_link_class": "stdlink", + "_titre_id": f"""titre-{acronyme_no_spaces}""", + "version": formation.version or 0, + } # Ajoute les semestres associés à chaque formation: row["formsemestres"] = formation.formsemestres.order_by( FormSemestre.date_debut diff --git a/app/scodoc/sco_formsemestre.py b/app/scodoc/sco_formsemestre.py index 0d9398e3..3d46b1de 100644 --- a/app/scodoc/sco_formsemestre.py +++ b/app/scodoc/sco_formsemestre.py @@ -38,7 +38,7 @@ import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu from app import log from app.models import Departement -from app.models import FormSemestre +from app.models import Formation, FormSemestre from app.scodoc import sco_cache, codes_cursus, sco_formations, sco_preferences from app.scodoc.gen_tables import GenTable from app.scodoc.codes_cursus import NO_SEMESTRE_ID @@ -145,13 +145,8 @@ def _formsemestre_enrich(sem): # imports ici pour eviter refs circulaires from app.scodoc import sco_formsemestre_edit - formations = sco_formations.formation_list( - args={"formation_id": sem["formation_id"]} - ) - if not formations: - raise ScoValueError("pas de formation pour ce semestre !") - F = formations[0] - parcours = codes_cursus.get_cursus_from_code(F["type_parcours"]) + formation: Formation = Formation.query.get_or_404(sem["formation_id"]) + parcours = codes_cursus.get_cursus_from_code(formation.type_parcours) # 'S1', 'S2', ... ou '' pour les monosemestres if sem["semestre_id"] != NO_SEMESTRE_ID: sem["sem_id_txt"] = "S%s" % sem["semestre_id"] diff --git a/app/scodoc/sco_formsemestre_exterieurs.py b/app/scodoc/sco_formsemestre_exterieurs.py index 5e8767a4..67f05d62 100644 --- a/app/scodoc/sco_formsemestre_exterieurs.py +++ b/app/scodoc/sco_formsemestre_exterieurs.py @@ -55,7 +55,6 @@ from app.scodoc import sco_formations from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_validation -from app.scodoc import sco_etud from app.scodoc.codes_cursus import UE_SPORT diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 1c52d4b0..38bc79d6 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -431,6 +431,12 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str: }, "enabled": formsemestre.can_edit_jury(), }, + { + "title": "Générer feuille préparation Jury (non BUT)", + "endpoint": "notes.feuille_preparation_jury", + "args": {"formsemestre_id": formsemestre_id}, + "enabled": not formsemestre.formation.is_apc(), + }, { "title": "Éditer les PV et archiver les résultats", "endpoint": "notes.formsemestre_archive", @@ -589,10 +595,7 @@ def formsemestre_description_table( ).first_or_404() nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id) - F = sco_formations.formation_list(args={"formation_id": formsemestre.formation_id})[ - 0 - ] - parcours = codes_cursus.get_cursus_from_code(F["type_parcours"]) + parcours = codes_cursus.get_cursus_from_code(formsemestre.formation.type_parcours) # --- Colonnes à proposer: columns_ids = ["UE", "Code", "Module"] if with_parcours: diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py index f586f44a..6f0f3e7a 100644 --- a/app/scodoc/sco_formsemestre_validation.py +++ b/app/scodoc/sco_formsemestre_validation.py @@ -64,7 +64,7 @@ from app.scodoc import sco_cursus_dut from app.scodoc.sco_cursus_dut import etud_est_inscrit_ue from app.scodoc import sco_photos from app.scodoc import sco_preferences -from app.scodoc import sco_dict_pv_jury +from app.scodoc import sco_pv_dict # ------------------------------------------------------------------------------------ def formsemestre_validation_etud_form( @@ -561,7 +561,7 @@ def formsemestre_recap_parcours_table( is_cur = Se.formsemestre_id == sem["formsemestre_id"] num_sem += 1 - dpv = sco_dict_pv_jury.dict_pvjury(sem["formsemestre_id"], etudids=[etudid]) + dpv = sco_pv_dict.dict_pvjury(sem["formsemestre_id"], etudids=[etudid]) pv = dpv["decisions"][0] decision_sem = pv["decision_sem"] decisions_ue = pv["decisions_ue"] diff --git a/app/scodoc/sco_groups_view.py b/app/scodoc/sco_groups_view.py index c149e90c..bfb75902 100644 --- a/app/scodoc/sco_groups_view.py +++ b/app/scodoc/sco_groups_view.py @@ -795,8 +795,7 @@ def groups_table( ) m["parcours"] = Se.get_cursus_descr() m["code_cursus"], _ = sco_report.get_code_cursus_etud(etud) - - L = [[m.get(k, "") for k in keys] for m in groups_infos.members] + rows = [[m.get(k, "") for k in keys] for m in groups_infos.members] title = "etudiants_%s" % groups_infos.groups_filename xls = sco_excel.excel_simple_table(titles=titles, lines=rows, sheet_name=title) filename = title diff --git a/app/scodoc/sco_inscr_passage.py b/app/scodoc/sco_inscr_passage.py index eafb63cc..1ebf679f 100644 --- a/app/scodoc/sco_inscr_passage.py +++ b/app/scodoc/sco_inscr_passage.py @@ -47,7 +47,7 @@ from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_groups from app.scodoc import sco_preferences -from app.scodoc import sco_dict_pv_jury +from app.scodoc import sco_pv_dict from app.scodoc.sco_exceptions import ScoValueError @@ -137,7 +137,7 @@ def list_inscrits(formsemestre_id, with_dems=False): def list_etuds_from_sem(src, dst) -> list[dict]: """Liste des etudiants du semestre src qui sont autorisés à passer dans le semestre dst.""" target = dst["semestre_id"] - dpv = sco_dict_pv_jury.dict_pvjury(src["formsemestre_id"]) + dpv = sco_pv_dict.dict_pvjury(src["formsemestre_id"]) if not dpv: return [] etuds = [ @@ -261,8 +261,8 @@ def list_source_sems(sem, delai=None) -> list[dict]: if s["semestre_id"] == codes_cursus.NO_SEMESTRE_ID: continue # - F = sco_formations.formation_list(args={"formation_id": s["formation_id"]})[0] - parcours = codes_cursus.get_cursus_from_code(F["type_parcours"]) + formation: Formation = Formation.query.get_or_404(s["formation_id"]) + parcours = codes_cursus.get_cursus_from_code(formation.type_parcours) if not parcours.ALLOW_SEM_SKIP: if s["semestre_id"] < (sem["semestre_id"] - 1): continue diff --git a/app/scodoc/sco_moduleimpl_inscriptions.py b/app/scodoc/sco_moduleimpl_inscriptions.py index a650033a..196051b0 100644 --- a/app/scodoc/sco_moduleimpl_inscriptions.py +++ b/app/scodoc/sco_moduleimpl_inscriptions.py @@ -402,7 +402,9 @@ def moduleimpl_inscriptions_stats(formsemestre_id): # Etudiants "dispensés" d'une UE (capitalisée) ues_cap_info = get_etuds_with_capitalized_ue(formsemestre_id) if ues_cap_info: - H.append('

    Étudiants avec UEs capitalisées:

      ') + H.append( + '

      Étudiants avec UEs capitalisées (ADM):

        ' + ) ues = [ sco_edit_ue.ue_list({"ue_id": ue_id})[0] for ue_id in ues_cap_info.keys() ] @@ -470,8 +472,9 @@ def moduleimpl_inscriptions_stats(formsemestre_id): if can_change: H.append( f""" """ ) diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py index 0aac0b18..a70cb0f4 100644 --- a/app/scodoc/sco_moduleimpl_status.py +++ b/app/scodoc/sco_moduleimpl_status.py @@ -190,14 +190,9 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): if not isinstance(moduleimpl_id, int): raise ScoInvalidIdType("moduleimpl_id must be an integer !") modimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id) - mi_dict = modimpl.to_dict() + module: Module = modimpl.module formsemestre_id = modimpl.formsemestre_id formsemestre: FormSemestre = modimpl.formsemestre - mod_dict = sco_edit_module.module_list(args={"module_id": modimpl.module_id})[0] - sem = sco_formsemestre.get_formsemestre(formsemestre_id) - formation_dict = sco_formations.formation_list( - args={"formation_id": sem["formation_id"]} - )[0] mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list( moduleimpl_id=moduleimpl_id ) @@ -232,22 +227,22 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags() # module_resp = User.query.get(modimpl.responsable_id) - mod_type_name = scu.MODULE_TYPE_NAMES[mod_dict["module_type"]] + mod_type_name = scu.MODULE_TYPE_NAMES[module.module_type] H = [ html_sco_header.sco_header( - page_title=f"{mod_type_name} {mod_dict['code']} {mod_dict['titre']}", + page_title=f"{mod_type_name} {module.code} {module.titre}", javascripts=["js/etud_info.js"], init_qtip=True, ), - f"""

        {mod_type_name} - {mod_dict['code']} {mod_dict['titre']} - {"dans l'UE " + modimpl.module.ue.acronyme - if modimpl.module.module_type == scu.ModuleType.MALUS + f"""

        {mod_type_name} + {module.code} {module.titre} + {"dans l'UE " + modimpl.module.ue.acronyme + if modimpl.module.module_type == scu.ModuleType.MALUS else "" }

        + scu.ModuleType(module.module_type).name.lower()}"> """) # 3ieme ligne: Formation H.append( - """""" - % formation_dict + f""" + + + """ ) # Ligne: Inscrits H.append( @@ -303,18 +300,17 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): ) if current_user.has_permission(Permission.ScoEtudInscrit): H.append( - """modifier""" - % mi_dict["moduleimpl_id"] + f"""modifier""" ) H.append("") # Ligne: règle de calcul - has_expression = sco_compute_moy.moduleimpl_has_expression(mi_dict) + has_expression = sco_compute_moy.moduleimpl_has_expression(modimpl) if has_expression: H.append( f""" - { newline.join(row.html() for row in self.head) } - - """ - if self.head - else "" - ) - footer = ( - f""" - - { newline.join(row.html() for row in self.foot) } - - """ - if self.foot - else "" - ) - return f""" - {header} - - { - newline.join(row.html() for row in self.rows) - } - - {footer} - """ - - def add_row(self, row: "Row") -> "Row": - """Append a new row""" - self.rows.append(row) - self.row_by_id[row.id] = row - return row - - def add_head_row(self, row: "Row") -> "Row": - "Add a row to table head" - # row = Row(self, cell_elt="th", category="head") - self.head.append(row) - self.row_by_id[row.id] = row - return row - - def add_foot_row(self, row: "Row") -> "Row": - "Add a row to table foot" - self.foot.append(row) - self.row_by_id[row.id] = row - return row - - def sort_rows(self, key: callable, reverse: bool = False): - """Sort table rows""" - self.rows.sort(key=key, reverse=reverse) - - def sort_columns(self): - """Sort columns ids""" - groups_order = {group: i for i, group in enumerate(self.groups)} - cols_order = {col_id: i for i, col_id in enumerate(self.column_ids)} - self.column_ids.sort( - key=lambda col_id: ( - groups_order.get(self.column_group.get(col_id), col_id), - cols_order[col_id], - ) - ) - - def insert_group(self, group: str, after: str = None, before: str = None): - """Déclare un groupe de colonnes et le place avant ou après un autre groupe. - Si pas d'autre groupe indiqué, le place après, à droite du dernier. - Si le group existe déjà, ne fait rien (ne le déplace pas). - """ - if group in self.groups: - return - other = after or before - if other is None: - self.groups.append(group) - else: - if not other in self.groups: - raise ValueError(f"invalid column group '{other}'") - index = self.groups.index(other) - if after: - index += 1 - self.groups.insert(index, group) - - def set_groups(self, groups: list[str]): - """Define column groups and set order""" - self.groups = groups - - def set_titles(self, titles: dict[str, str]): - """Set columns titles""" - self.titles = titles - - def update_titles(self, titles: dict[str, str]): - """Set columns titles""" - self.titles.update(titles) - - def add_title( - self, col_id, title: str = None, classes: list[str] = None - ) -> tuple["Cell", "Cell"]: - """Record this title, - and create cells for footer and header if they don't already exist. - """ - title = title or "" - if col_id not in self.titles: - self.titles[col_id] = title - self.head_title_row.cells[col_id] = self.head_title_row.add_cell( - col_id, None, title, classes=classes - ) - self.foot_title_row.cells[col_id] = self.foot_title_row.add_cell( - col_id, None, title, classes=classes - ) - - return self.head_title_row.cells.get(col_id), self.foot_title_row.cells[col_id] - - -class Row(Element): - """A row.""" - - def __init__( - self, - table: Table, - row_id=None, - category=None, - cell_elt: str = None, - classes: list[str] = None, - attrs: dict[str, str] = None, - data: dict = None, - ): - super().__init__("tr", classes=classes, attrs=attrs, data=data) - self.category = category - self.cells = {} - self.cell_elt = cell_elt - self.classes: list[str] = classes or [] - "classes sur le " - self.id = row_id - self.table = table - - def add_cell( - self, - col_id: str, - title: str, - content: str, - group: str = None, - attrs: list[str] = None, - classes: list[str] = None, - data: dict[str, str] = None, - elt: str = None, - raw_content=None, - target_attrs: dict = None, - target: str = None, - ) -> "Cell": - """Create cell and add it to the row. - group: groupe de colonnes - classes is a list of css class names - """ - cell = Cell( - content, - (classes or []) + [group or ""], # ajoute le nom de groupe aux classes - elt=elt or self.cell_elt, - attrs=attrs, - data=data, - raw_content=raw_content, - target=target, - target_attrs=target_attrs, - ) - return self.add_cell_instance(col_id, cell, column_group=group, title=title) - - def add_cell_instance( - self, col_id: str, cell: "Cell", column_group: str = None, title: str = None - ) -> "Cell": - """Add a cell to the row. - Si title est None, il doit avoir été ajouté avec table.add_title(). - """ - cell.data["group"] = column_group - self.cells[col_id] = cell - if col_id not in self.table.column_ids: - self.table.column_ids.append(col_id) - self.table.insert_group(column_group) - if column_group is not None: - self.table.column_group[col_id] = column_group - - if title is not None: - self.table.add_title(col_id, title, classes=cell.classes) - - return cell - - def html(self, extra_classes: list[str] = None) -> str: - """html for row, with cells""" - if (self.id is not None) and self.id == getattr(self.table, "selected_row_id"): - self.classes.append("row_selected") - return super().html(extra_classes=extra_classes) - - def html_content(self) -> str: - "Le contenu du row en html." - return "".join( - [ - self.cells.get(col_id, self.table.empty_cell).html( - extra_classes=self.table.column_classes.get(col_id) - ) - for col_id in self.table.column_ids - ] - ) - - def to_dict(self) -> dict: - """row as a dict, with only cell contents""" - return { - col_id: self.cells.get(col_id, self.table.empty_cell).raw_content - for col_id in self.table.column_ids - } - - -class BottomRow(Row): - """Une ligne spéciale pour le pied de table - avec un titre à gauche - (répété sur les colonnes indiquées par left_title_col_ids), - et automatiquement ajouté au footer. - """ - - def __init__( - self, *args, left_title_col_ids: list[str] = None, left_title=None, **kwargs - ): - super().__init__(*args, **kwargs) - self.left_title_col_ids = left_title_col_ids - if left_title is not None: - self.set_left_title(left_title) - self.table.add_foot_row(self) - - def set_left_title(self, title: str = ""): - "Fill left title cells" - for col_id in self.left_title_col_ids: - self.add_cell(col_id, None, title) - - -class Cell(Element): - """Une cellule de table""" - - def __init__( - self, - content, - classes: list[str] = None, - elt="td", - attrs: dict[str, str] = None, - data: dict = None, - raw_content=None, - target: str = None, - target_attrs: dict = None, - ): - """if specified, raw_content will be used for raw exports like xlsx""" - super().__init__( - elt if elt is not None else "td", content, classes, attrs, data - ) - if self.elt == "th": - self.attrs["scope"] = "row" - - self.data = data or {} - self.raw_content = raw_content or content - self.target = target - self.target_attrs = target_attrs or {} - - @classmethod - def empty(cls): - "create a new empty cell" - return cls("") - - def __str__(self): - return str(self.content) - - def html_content(self) -> str: - "content of the table cell, as html" - # entoure le contenu par un lien ? - if (self.target is not None) or self.target_attrs: - href = f'href="{self.target}"' if self.target else "" - target_attrs_str = " ".join( - [f'{k}="{v}"' for (k, v) in self.target_attrs.items()] - ) - return f"{super().html_content()}" - - return super().html_content() diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index eaecffe8..d4674f3c 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -4171,10 +4171,29 @@ table.table_recap .cursus { white-space: nowrap; } -table.table_recap .col_ue, -table.table_recap .col_ue_code, -table.table_recap .col_moy_gen, -table.table_recap .group { +table.table_recap td.col_rcue, +table.table_recap th.col_rcue, +table.table_recap td.cursus_but.first, +table.table_recap td.cursus_but.first { + border-left: 1px solid rgb(221, 221, 221); +} + +table.table_recap td.cursus_BUT1 { + color: #007bff; +} + +table.table_recap td.cursus_BUT2 { + color: #d39f00; +} + +table.table_recap td.cursus_BUT3 { + color: #7f00ff; +} + +table.table_recap td.col_ue, +table.table_recap td.col_ue_code, +table.table_recap td.col_moy_gen, +table.table_recap td.group { border-left: 1px solid blue; } diff --git a/app/static/js/jury_but.js b/app/static/js/jury_but.js index df6d5b9e..c53b223a 100644 --- a/app/static/js/jury_but.js +++ b/app/static/js/jury_but.js @@ -1,80 +1,94 @@ + + // active les menus des codes "manuels" (année, RCUEs) function enable_manual_codes(elt) { - $(".jury_but select.manual").prop("disabled", !elt.checked); + $(".jury_but select.manual").prop("disabled", !elt.checked); } // changement d'un menu code: function change_menu_code(elt) { - // Ajuste styles pour visualiser codes enregistrés/modifiés - if (elt.value != elt.dataset.orig_code) { - elt.parentElement.parentElement.classList.add("modified"); - } else { - elt.parentElement.parentElement.classList.remove("modified"); - } - if (elt.value == elt.dataset.orig_recorded) { - elt.parentElement.parentElement.classList.add("recorded"); - } else { - elt.parentElement.parentElement.classList.remove("recorded"); - } - // Si RCUE passant en ADJ, change les menus des UEs associées ADJR - if ( - elt.classList.contains("code_rcue") && - elt.dataset.niveau_id && - elt.value == "ADJ" && - elt.value != elt.dataset.orig_recorded - ) { - let ue_selects = - elt.parentElement.parentElement.parentElement.querySelectorAll( - "select.ue_rcue_" + elt.dataset.niveau_id - ); - ue_selects.forEach((select) => { - if (select.value != "ADM") { - select.value = "ADJR"; - change_menu_code(select); // pour changer les styles - } - }); - } + // Ajuste styles pour visualiser codes enregistrés/modifiés + if (elt.value != elt.dataset.orig_code) { + elt.parentElement.parentElement.classList.add("modified"); + } else { + elt.parentElement.parentElement.classList.remove("modified"); + } + if (elt.value == elt.dataset.orig_recorded) { + elt.parentElement.parentElement.classList.add("recorded"); + } else { + elt.parentElement.parentElement.classList.remove("recorded"); + } + // Si RCUE passant en ADJ, change les menus des UEs associées ADJR + if (elt.classList.contains("code_rcue") + && elt.dataset.niveau_id + && elt.value == "ADJ" + && elt.value != elt.dataset.orig_recorded) { + let ue_selects = elt.parentElement.parentElement.parentElement.querySelectorAll( + "select.ue_rcue_" + elt.dataset.niveau_id); + ue_selects.forEach(select => { + if (select.value != "ADM") { + select.value = "ADJR"; + change_menu_code(select); // pour changer les styles + } + }); + } } $(function () { - // Recupère la liste ordonnées des etudids - // pour avoir le "suivant" etr le "précédent" - // (liens de navigation) - const url = new URL(document.URL); - const frags = url.pathname.split("/"); // .../formsemestre_validation_but/formsemestre_id/etudid - const etudid = frags[frags.length - 1]; - const formsemestre_id = frags[frags.length - 2]; - const etudids_key = JSON.stringify(["etudids", url.origin, formsemestre_id]); - const etudids_str = localStorage.getItem(etudids_key); - const noms_key = JSON.stringify(["noms", url.origin, formsemestre_id]); - const noms_str = localStorage.getItem(noms_key); - if (etudids_str && noms_str) { - const etudids = JSON.parse(etudids_str); - const noms = JSON.parse(noms_str); - const cur_idx = etudids.indexOf(etudid); - let prev_idx = -1; - let next_idx = -1; - if (cur_idx != -1) { - if (cur_idx > 0) { - prev_idx = cur_idx - 1; - } - if (cur_idx < etudids.length - 1) { - next_idx = cur_idx + 1; - } - } - if (prev_idx != -1) { - let elem = document.querySelector("div.prev a"); - if (elem) { - elem.href = elem.href.replace("PREV", etudids[prev_idx]); - elem.innerHTML = noms[prev_idx]; - } + // Recupère la liste ordonnées des etudids + // pour avoir le "suivant" et le "précédent" + // (liens de navigation) + const url = new URL(document.URL); + const frags = url.pathname.split("/"); // .../formsemestre_validation_but/formsemestre_id/etudid + const etudid = frags[frags.length - 1]; + const formsemestre_id = frags[frags.length - 2]; + const etudids_key = JSON.stringify(["etudids", url.origin, formsemestre_id]); + const etudids_str = localStorage.getItem(etudids_key); + const noms_key = JSON.stringify(["noms", url.origin, formsemestre_id]); + const noms_str = localStorage.getItem(noms_key); + if (etudids_str && noms_str) { + const etudids = JSON.parse(etudids_str); + const noms = JSON.parse(noms_str); + const cur_idx = etudids.indexOf(etudid); + let prev_idx = -1; + let next_idx = -1 + if (cur_idx != -1) { + if (cur_idx > 0) { + prev_idx = cur_idx - 1; + } + if (cur_idx < etudids.length - 1) { + next_idx = cur_idx + 1; + } + } + if (prev_idx != -1) { + let elem = document.querySelector("div.prev a"); + if (elem) { + elem.href = elem.href.replace("PREV", etudids[prev_idx]); + elem.innerHTML = noms[prev_idx]; + } + } else { + document.querySelector("div.prev").innerHTML = ""; + } + if (next_idx != -1) { + let elem = document.querySelector("div.next a"); + if (elem) { + elem.href = elem.href.replace("NEXT", etudids[next_idx]); + elem.innerHTML = noms[next_idx]; + } + } else { + document.querySelector("div.next").innerHTML = ""; + } } else { - document.querySelector("div.prev").innerHTML = ""; + // Supprime les liens de navigation + document.querySelector("div.prev").innerHTML = ""; + document.querySelector("div.next").innerHTML = ""; } }); // ----- Etat du formulaire jury pour éviter sortie sans enregistrer let FORM_STATE = ""; +let IS_SUBMITTING = false; + // Une chaine décrivant l'état du form function get_form_state() { let codes = []; @@ -85,13 +99,19 @@ function get_form_state() { $('document').ready(function () { FORM_STATE = get_form_state(); + document.querySelector("form#jury_but").addEventListener('submit', jury_form_submit); }); function is_modified() { return FORM_STATE != get_form_state(); } + +function jury_form_submit(event) { + IS_SUBMITTING = true; +} + window.addEventListener("beforeunload", function (e) { - if (is_modified()) { + if ((!IS_SUBMITTING) && is_modified()) { var confirmationMessage = 'Changements non enregistrés !'; (e || window.event).returnValue = confirmationMessage; return confirmationMessage; diff --git a/app/static/js/table_recap.js b/app/static/js/table_recap.js index 1c87ffd7..56d43e3d 100644 --- a/app/static/js/table_recap.js +++ b/app/static/js/table_recap.js @@ -47,26 +47,30 @@ $(function () { } }); } - } - } - // Les colonnes visibles sont mémorisées, il faut initialiser l'état des boutons - function update_buttons_labels(dt) { - // chaque bouton controle une classe stockée dans le data-group du span - document.querySelectorAll("button.dt-button").forEach((but) => { - let g_span = but.querySelector("span > span"); - if (g_span) { - let group = g_span.dataset["group"]; - if (group) { - // si le group (= la 1ere col.) est visible, but_on - if (dt.columns("." + group).visible()[0]) { - but.classList.add("but_on"); - but.classList.remove("but_off"); + // Changement visibilité groupes colonnes (boutons) + function toggle_col_but_visibility(e, dt, node, config) { + let group = node.children()[0].firstChild.dataset.group; + toggle_col_group_visibility(dt, group, node.hasClass("but_on")); + } + function toggle_col_ident_visibility(e, dt, node, config) { + let onoff = node.hasClass("but_on"); + toggle_col_group_visibility(dt, "identite_detail", onoff); + toggle_col_group_visibility(dt, "identite_court", !onoff); + } + function toggle_col_ressources_visibility(e, dt, node, config) { + let onoff = node.hasClass("but_on"); + toggle_col_group_visibility(dt, "col_res", onoff); + toggle_col_group_visibility(dt, "col_ue_bonus", onoff); + toggle_col_group_visibility(dt, "col_malus", onoff); + } + function toggle_col_group_visibility(dt, group, onoff) { + if (onoff) { + dt.columns('.' + group).visible(false); } else { - but.classList.add("but_off"); - but.classList.remove("but_on"); + dt.columns('.' + group).visible(true); } - } + update_buttons_labels(dt); } // Definition des boutons au dessus de la table: let buttons = [ @@ -102,11 +106,11 @@ $(function () { action: toggle_col_ident_visibility, }, { - text: 'Groupes', + text: 'Groupes', action: toggle_col_but_visibility, }, { - text: 'Rg', + text: 'Rg', action: toggle_col_but_visibility, }, ]; // fin des boutons communs à toutes les tables recap @@ -156,19 +160,21 @@ $(function () { action: toggle_col_but_visibility, }); } - : { - name: "toggle_mod", - text: "Cacher les modules", - action: function (e, dt, node, config) { - let onoff = node.hasClass("but_on"); - toggle_col_group_visibility( - dt, - "col_mod:not(.col_empty)", - onoff + // S'il y a des colonnes vides: + if ($('table.table_recap td.col_empty').length > 0) { + buttons.push({ // modules vides + text: 'Vides', + action: toggle_col_but_visibility, + }); + } + // Boutons admission (pas en jury) + if (!$('table.table_recap').hasClass("jury")) { + buttons.push( + { + text: 'Admission', + action: toggle_col_but_visibility, + } ); - toggle_col_group_visibility(dt, "col_ue_bonus", onoff); - toggle_col_group_visibility(dt, "col_malus", onoff); - }, } } // Boutons évaluations (si présentes) @@ -230,7 +236,8 @@ $(function () { buttons: buttons, "drawCallback": function (settings) { // permet de conserver l'ordre de tri des colonnes - let order_info = JSON.stringify($('table.table_recap').DataTable().order()); + let table = $('table.table_recap').DataTable(); + let order_info = JSON.stringify(table.order()); if (formsemestre_id) { localStorage.setItem(order_info_key, order_info); } @@ -270,114 +277,9 @@ $(function () { $(function () { let row_selected = document.querySelector(".row_selected"); if (row_selected) { - /*row_selected.scrollIntoView(); - window.scrollBy(0, -50);*/ + row_selected.scrollIntoView(); + window.scrollBy(0, -125); row_selected.classList.add("selected"); } - - // ------------- LA TABLE --------- - try { - let table = $("table.table_recap").DataTable({ - paging: false, - searching: true, - info: false, - autoWidth: false, - fixedHeader: { - header: true, - footer: false, - }, - orderCellsTop: true, // cellules ligne 1 pour tri - aaSorting: [], // Prevent initial sorting - colReorder: true, - stateSave: true, // enregistre état de la table (tris, ...) - columnDefs: [ - { - // cache les codes, le détail de l'identité, les groupes, les colonnes admission et les vides - targets: hidden_colums, - visible: false, - }, - { - // Elimine les 0 à gauche pour les exports excel et les "copy" - targets: [ - "col_mod", - "col_moy_gen", - "col_moy_ue", - "col_res", - "col_sae", - "evaluation", - "col_rcue", - ], - render: function (data, type, row) { - return type === "export" ? data.replace(/0(\d\..*)/, "$1") : data; - }, - }, - { - // Elimine les "+"" pour les exports - targets: ["col_ue_bonus", "col_malus"], - render: function (data, type, row) { - return type === "export" - ? data - .replace(/.*\+(\d?\d?\.\d\d).*/m, "$1") - .replace(/0(\d\..*)/, "$1") - : data; - }, - }, - { - // Elimine emoji warning sur UEs - targets: ["col_ues_validables"], - render: function (data, type, row) { - return type === "export" - ? data.replace(/(\d+\/\d+).*/, "$1") - : data; - }, - }, - ], - dom: "Bfrtip", - buttons: buttons, - drawCallback: function (settings) { - // permet de conserver l'ordre de tri des colonnes - let table = $("table.table_recap").DataTable(); - let order_info = JSON.stringify(table.order()); - if (formsemestre_id) { - localStorage.setItem(order_info_key, order_info); - } - let etudids = []; - document.querySelectorAll("td.identite_court").forEach((e) => { - etudids.push(e.dataset.etudid); - }); - let noms = []; - document.querySelectorAll("td.identite_court").forEach((e) => { - noms.push(e.dataset.nomprenom); - }); - localStorage.setItem(etudids_key, JSON.stringify(etudids)); - localStorage.setItem(noms_key, JSON.stringify(noms)); - }, - order: order_info, - }); - update_buttons_labels(table); - } catch (error) { - // l'erreur peut etre causee par un ancien storage: - localStorage.removeItem(etudids_key); - localStorage.removeItem(noms_key); - localStorage.removeItem(order_info_key); - location.reload(); - } - }); - $("table.table_recap tbody").on("click", "tr", function () { - if ($(this).hasClass("selected")) { - $(this).removeClass("selected"); - } else { - $("table.table_recap tr.selected").removeClass("selected"); - $(this).addClass("selected"); - } - }); - // Pour montrer et surligner l'étudiant sélectionné: - $(function () { - let row_selected = document.querySelector(".row_selected"); - if (row_selected) { - row_selected.scrollIntoView(); - window.scrollBy(0, -125); - row_selected.classList.add("selected"); - } - }); + }); }); diff --git a/app/tables/jury_recap.py b/app/tables/jury_recap.py index 0e91ddbe..2be036a6 100644 --- a/app/tables/jury_recap.py +++ b/app/tables/jury_recap.py @@ -23,7 +23,6 @@ from app.comp.res_compat import NotesTableCompat from app.models import ApcNiveau, UniteEns from app.models.etudiants import Identite from app.models.formsemestre import FormSemestre -from app.scodoc import html_sco_header from app.scodoc.codes_cursus import ( BUT_BARRE_RCUE, BUT_RCUE_SUFFISANT, diff --git a/app/tables/recap.py b/app/tables/recap.py index 3449aaa0..989629ae 100644 --- a/app/tables/recap.py +++ b/app/tables/recap.py @@ -12,8 +12,7 @@ import numpy as np from app.auth.models import User from app.comp.res_common import ResultatsSemestre -from app.models import Identite -from app.models.ues import UniteEns +from app.models import Identite, FormSemestre, UniteEns from app.scodoc.codes_cursus import UE_SPORT, DEF from app.scodoc import sco_evaluation_db from app.scodoc import sco_groups diff --git a/app/templates/pn/form_mods.j2 b/app/templates/pn/form_mods.j2 index f88140f1..21677f40 100644 --- a/app/templates/pn/form_mods.j2 +++ b/app/templates/pn/form_mods.j2 @@ -49,7 +49,7 @@ ({{mod.ue.acronyme}}), {% endif %} - - parcours {{ mod.get_cursus()|map(attribute="code")|join(", ")|default('tronc commun', + - parcours {{ mod.get_parcours()|map(attribute="code")|join(", ")|default('tronc commun', true)|safe }} {% if mod.heures_cours or mod.heures_td or mod.heures_tp %} diff --git a/app/views/notes.py b/app/views/notes.py index 4116cb7a..046e078f 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -1590,10 +1590,24 @@ def etud_desinscrit_ue(etudid, formsemestre_id, ue_id): ue = UniteEns.query.get_or_404(ue_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id) if ue.formation.is_apc(): - if DispenseUE.query.filter_by(etudid=etudid, ue_id=ue_id).count() == 0: - disp = DispenseUE(ue_id=ue_id, etudid=etudid) + if ( + DispenseUE.query.filter_by( + formsemestre_id=formsemestre_id, etudid=etudid, ue_id=ue_id + ).count() + == 0 + ): + disp = DispenseUE( + formsemestre_id=formsemestre_id, ue_id=ue_id, etudid=etudid + ) db.session.add(disp) db.session.commit() + log(f"etud_desinscrit_ue {etud} {ue}") + Scolog.logdb( + method="etud_desinscrit_ue", + etudid=etud.id, + msg=f"Désinscription de l'UE {ue.acronyme} de {formsemestre.titre_annee()}", + commit=True, + ) sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id) else: sco_moduleimpl_inscriptions.do_etud_desinscrit_ue_classic( diff --git a/migrations/versions/dbcf2175e87f_modèles_assiduites_justificatifs.py b/migrations/versions/dbcf2175e87f_modèles_assiduites_justificatifs.py index 9463f6b5..b74fa27d 100644 --- a/migrations/versions/dbcf2175e87f_modèles_assiduites_justificatifs.py +++ b/migrations/versions/dbcf2175e87f_modèles_assiduites_justificatifs.py @@ -11,7 +11,7 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. revision = "dbcf2175e87f" -down_revision = "d8288b7f0a3e" +down_revision = "6520faf67508" branch_labels = None depends_on = None diff --git a/tests/unit/sco_fake_gen.py b/tests/unit/sco_fake_gen.py index b7714368..48712483 100644 --- a/tests/unit/sco_fake_gen.py +++ b/tests/unit/sco_fake_gen.py @@ -19,7 +19,6 @@ from app.auth.models import User from app.models import Departement, Formation, FormationModalite, Matiere from app.scodoc import notesdb as ndb from app.scodoc import codes_cursus -from app.scodoc import sco_edit_formation from app.scodoc import sco_edit_matiere from app.scodoc import sco_edit_module from app.scodoc import sco_edit_ue @@ -154,7 +153,7 @@ class ScoFake(object): acronyme="test", titre="Formation test", titre_officiel="Le titre officiel de la formation test", - type_parcours=codes_cursus.CursusDUT.TYPE_CURSUS, + type_parcours: int = codes_cursus.CursusDUT.TYPE_CURSUS, formation_code=None, code_specialite=None, ) -> int: diff --git a/tests/unit/test_but_ues.py b/tests/unit/test_but_ues.py index 693c2201..ae850b53 100644 --- a/tests/unit/test_but_ues.py +++ b/tests/unit/test_but_ues.py @@ -123,7 +123,13 @@ def test_ue_moy(test_client): modimpl.module.ue.type != UE_SPORT for modimpl in formsemestre.modimpls_sorted ] etud_moy_ue = moy_ue.compute_ue_moys_apc( - sem_cube, etuds, modimpls, modimpl_inscr_df, modimpl_coefs_df, modimpl_mask + sem_cube, + etuds, + modimpls, + modimpl_inscr_df, + modimpl_coefs_df, + modimpl_mask, + set(), ) assert etud_moy_ue[ue1.id][etudid] == n1 assert etud_moy_ue[ue2.id][etudid] == n1
        Responsable: @@ -294,8 +289,10 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): H.append("""
        Formation: %(titre)s
        Formation: {formsemestre.formation.titre}
        Règle de calcul: moyenne={mi_dict["computation_expr"]} + >moyenne={modimpl.computation_expr} """ ) H.append("""inutilisée dans cette version de ScoDoc""") @@ -421,21 +417,20 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):

        """ - % mi_dict ) # -------- Tableau des evaluations top_table_links = "" if can_edit_evals: top_table_links = f"""Créer nouvelle évaluation """ if nb_evaluations > 0: top_table_links += f""" Trier par date """ diff --git a/app/scodoc/sco_pv_forms.py b/app/scodoc/sco_pv_forms.py index b1f9a404..5b7be89e 100644 --- a/app/scodoc/sco_pv_forms.py +++ b/app/scodoc/sco_pv_forms.py @@ -38,18 +38,13 @@ import flask from flask import flash, redirect, url_for from flask import g, request -from app.models import ( - Formation, - FormSemestre, - ScolarAutorisationInscription, -) -from app.models.etudiants import Identite +from app.models import FormSemestre, Identite import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app.scodoc import html_sco_header from app.scodoc import codes_cursus -from app.scodoc import sco_dict_pv_jury +from app.scodoc import sco_pv_dict from app.scodoc import sco_etud from app.scodoc import sco_groups from app.scodoc import sco_groups_view @@ -230,7 +225,7 @@ def formsemestre_pvjury(formsemestre_id, format="html", publish=True): footer = html_sco_header.sco_footer() - dpv = sco_dict_pv_jury.dict_pvjury(formsemestre_id, with_prev=True) + dpv = sco_pv_dict.dict_pvjury(formsemestre_id, with_prev=True) if not dpv: if format == "html": return ( @@ -411,7 +406,7 @@ def formsemestre_pvjury_pdf(formsemestre_id, group_ids: list[int] = None, etudid tf[2]["anonymous"] = bool(tf[2]["anonymous"]) try: PDFLOCK.acquire() - pdfdoc = sco_pvpdf.pvjury_pdf( + pdfdoc = sco_pv_pdf.pvjury_pdf( formsemestre, etudids, numero_arrete=tf[2]["numero_arrete"], diff --git a/app/scodoc/sco_pvpdf.py b/app/scodoc/sco_pvpdf.py deleted file mode 100644 index 765d850f..00000000 --- a/app/scodoc/sco_pvpdf.py +++ /dev/null @@ -1,937 +0,0 @@ -# -*- 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 -# -############################################################################## - -"""Edition des PV de jury -""" -import io -import re - -from PIL import Image as PILImage -from PIL import UnidentifiedImageError - -import reportlab -from reportlab.lib.units import cm, mm -from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_JUSTIFY -from reportlab.platypus import Paragraph, Spacer, Frame, PageBreak -from reportlab.platypus import Table, TableStyle, Image -from reportlab.platypus.doctemplate import PageTemplate, BaseDocTemplate -from reportlab.lib.pagesizes import A4, landscape -from reportlab.lib import styles -from reportlab.lib.colors import Color - -from flask import g -from app.models import FormSemestre, Identite - -import app.scodoc.sco_utils as scu -from app.scodoc import sco_bulletins_pdf -from app.scodoc import codes_cursus -from app.scodoc import sco_dict_pv_jury -from app.scodoc import sco_etud -from app.scodoc import sco_pdf -from app.scodoc import sco_preferences -from app.scodoc.sco_exceptions import ScoValueError -from app.scodoc.sco_logos import find_logo -from app.scodoc.sco_cursus_dut import SituationEtudCursus -from app.scodoc.sco_pdf import SU -import sco_version - -LOGO_FOOTER_ASPECT = scu.CONFIG.LOGO_FOOTER_ASPECT # XXX A AUTOMATISER -LOGO_FOOTER_HEIGHT = scu.CONFIG.LOGO_FOOTER_HEIGHT * mm -LOGO_FOOTER_WIDTH = LOGO_FOOTER_HEIGHT * scu.CONFIG.LOGO_FOOTER_ASPECT - -LOGO_HEADER_ASPECT = scu.CONFIG.LOGO_HEADER_ASPECT # XXX logo IUTV (A AUTOMATISER) -LOGO_HEADER_HEIGHT = scu.CONFIG.LOGO_HEADER_HEIGHT * mm -LOGO_HEADER_WIDTH = LOGO_HEADER_HEIGHT * scu.CONFIG.LOGO_HEADER_ASPECT - - -def page_footer(canvas, doc, logo, preferences, with_page_numbers=True): - "Add footer on page" - width = doc.pagesize[0] # - doc.pageTemplate.left_p - doc.pageTemplate.right_p - foot = Frame( - 0.1 * mm, - 0.2 * cm, - width - 1 * mm, - 2 * cm, - leftPadding=0, - rightPadding=0, - topPadding=0, - bottomPadding=0, - id="monfooter", - showBoundary=0, - ) - - left_foot_style = reportlab.lib.styles.ParagraphStyle({}) - left_foot_style.fontName = preferences["SCOLAR_FONT"] - left_foot_style.fontSize = preferences["SCOLAR_FONT_SIZE_FOOT"] - left_foot_style.leftIndent = 0 - left_foot_style.firstLineIndent = 0 - left_foot_style.alignment = TA_RIGHT - right_foot_style = reportlab.lib.styles.ParagraphStyle({}) - right_foot_style.fontName = preferences["SCOLAR_FONT"] - right_foot_style.fontSize = preferences["SCOLAR_FONT_SIZE_FOOT"] - right_foot_style.alignment = TA_RIGHT - - p = sco_pdf.make_paras( - f"""{preferences["INSTITUTION_NAME"]}{ - preferences["INSTITUTION_ADDRESS"]}""", - left_foot_style, - ) - - np = Paragraph(f'{doc.page}', right_foot_style) - tabstyle = TableStyle( - [ - ("LEFTPADDING", (0, 0), (-1, -1), 0), - ("RIGHTPADDING", (0, 0), (-1, -1), 0), - ("ALIGN", (0, 0), (-1, -1), "RIGHT"), - # ('INNERGRID', (0,0), (-1,-1), 0.25, black),#debug - # ('LINEABOVE', (0,0), (-1,0), 0.5, black), - ("VALIGN", (1, 0), (1, 0), "MIDDLE"), - ("RIGHTPADDING", (-1, 0), (-1, 0), 1 * cm), - ] - ) - elems = [p] - if logo: - elems.append(logo) - colWidths = [None, LOGO_FOOTER_WIDTH + 2 * mm] - if with_page_numbers: - elems.append(np) - colWidths.append(2 * cm) - else: - elems.append("") - colWidths.append(8 * mm) # force marge droite - tab = Table([elems], style=tabstyle, colWidths=colWidths) - canvas.saveState() # is it necessary ? - foot.addFromList([tab], canvas) - canvas.restoreState() - - -def page_header(canvas, doc, logo, preferences, only_on_first_page=False): - "Ajoute au canvas le frame avec le logo" - if only_on_first_page and int(doc.page) > 1: - return - height = doc.pagesize[1] - head = Frame( - -22 * mm, - height - 13 * mm - LOGO_HEADER_HEIGHT, - 10 * cm, - LOGO_HEADER_HEIGHT + 2 * mm, - leftPadding=0, - rightPadding=0, - topPadding=0, - bottomPadding=0, - id="monheader", - showBoundary=0, - ) - if logo: - canvas.saveState() # is it necessary ? - head.addFromList([logo], canvas) - canvas.restoreState() - - -class CourrierIndividuelTemplate(PageTemplate): - """Template pour courrier avisant des decisions de jury (1 page par étudiant)""" - - def __init__( - self, - document, - pagesbookmarks=None, - author=None, - title=None, - subject=None, - margins=(0, 0, 0, 0), # additional margins in mm (left,top,right, bottom) - preferences=None, # dictionnary with preferences, required - force_header=False, - force_footer=False, # always add a footer (whatever the preferences, use for PV) - template_name="CourrierJuryTemplate", - ): - """Initialise our page template.""" - self.pagesbookmarks = pagesbookmarks or {} - self.pdfmeta_author = author - self.pdfmeta_title = title - self.pdfmeta_subject = subject - self.preferences = preferences - self.force_header = force_header - self.force_footer = force_footer - self.with_footer = ( - self.force_footer or self.preferences["PV_LETTER_WITH_HEADER"] - ) - self.with_header = ( - self.force_header or self.preferences["PV_LETTER_WITH_FOOTER"] - ) - self.with_page_background = self.preferences["PV_LETTER_WITH_BACKGROUND"] - self.with_page_numbers = False - self.header_only_on_first_page = False - # Our doc is made of a single frame - left, top, right, bottom = margins # marge additionnelle en mm - # marges du Frame principal - self.bot_p = 2 * cm - self.left_p = 2.5 * cm - self.right_p = 2.5 * cm - self.top_p = 0 * cm - # log("margins=%s" % str(margins)) - content = Frame( - self.left_p + left * mm, - self.bot_p + bottom * mm, - document.pagesize[0] - self.right_p - self.left_p - left * mm - right * mm, - document.pagesize[1] - self.top_p - self.bot_p - top * mm - bottom * mm, - ) - - PageTemplate.__init__(self, template_name, [content]) - - self.background_image_filename = None - self.logo_footer = None - self.logo_header = None - # Search logos in dept specific dir, then in global scu.CONFIG dir - if template_name == "PVJuryTemplate": - background = find_logo( - logoname="pvjury_background", - dept_id=g.scodoc_dept_id, - ) or find_logo( - logoname="pvjury_background", - dept_id=g.scodoc_dept_id, - prefix="", - ) - else: - background = find_logo( - logoname="letter_background", - dept_id=g.scodoc_dept_id, - ) or find_logo( - logoname="letter_background", - dept_id=g.scodoc_dept_id, - prefix="", - ) - if not self.background_image_filename and background is not None: - self.background_image_filename = background.filepath - - footer = find_logo(logoname="footer", dept_id=g.scodoc_dept_id) - if footer is not None: - self.logo_footer = Image( - footer.filepath, - height=LOGO_FOOTER_HEIGHT, - width=LOGO_FOOTER_WIDTH, - ) - - header = find_logo(logoname="header", dept_id=g.scodoc_dept_id) - if header is not None: - self.logo_header = Image( - header.filepath, - height=LOGO_HEADER_HEIGHT, - width=LOGO_HEADER_WIDTH, - ) - - def beforeDrawPage(self, canv, doc): - """Draws a logo and an contribution message on each page.""" - # ---- Add some meta data and bookmarks - if self.pdfmeta_author: - canv.setAuthor(SU(self.pdfmeta_author)) - if self.pdfmeta_title: - canv.setTitle(SU(self.pdfmeta_title)) - if self.pdfmeta_subject: - canv.setSubject(SU(self.pdfmeta_subject)) - bm = self.pagesbookmarks.get(doc.page, None) - if bm != None: - key = bm - txt = SU(bm) - canv.bookmarkPage(key) - canv.addOutlineEntry(txt, bm) - - # ---- Background image - if self.background_image_filename and self.with_page_background: - canv.drawImage( - self.background_image_filename, 0, 0, doc.pagesize[0], doc.pagesize[1] - ) - - # ---- Header/Footer - if self.with_header: - page_header( - canv, - doc, - self.logo_header, - self.preferences, - self.header_only_on_first_page, - ) - if self.with_footer: - page_footer( - canv, - doc, - self.logo_footer, - self.preferences, - with_page_numbers=self.with_page_numbers, - ) - - -class PVTemplate(CourrierIndividuelTemplate): - """Template pour les pages des PV de jury""" - - def __init__( - self, - document, - author=None, - title=None, - subject=None, - margins=None, # additional margins in mm (left,top,right, bottom) - preferences=None, # dictionnary with preferences, required - ): - if margins is None: - margins = ( - preferences["pv_left_margin"], - preferences["pv_top_margin"], - preferences["pv_right_margin"], - preferences["pv_bottom_margin"], - ) - CourrierIndividuelTemplate.__init__( - self, - document, - author=author, - title=title, - subject=subject, - margins=margins, - preferences=preferences, - force_header=True, - force_footer=True, - template_name="PVJuryTemplate", - ) - self.with_page_numbers = True - self.header_only_on_first_page = True - self.with_header = self.preferences["PV_WITH_HEADER"] - self.with_footer = self.preferences["PV_WITH_FOOTER"] - self.with_page_background = self.preferences["PV_WITH_BACKGROUND"] - - def afterDrawPage(self, canv, doc): - """Called after all flowables have been drawn on a page""" - pass - - def beforeDrawPage(self, canv, doc): - """Called before any flowables are drawn on a page""" - # If the page number is even, force a page break - CourrierIndividuelTemplate.beforeDrawPage(self, canv, doc) - # Note: on cherche un moyen de generer un saut de page double - # (redémarrer sur page impaire, nouvelle feuille en recto/verso). Pas trouvé en Platypus. - # - # if self.__pageNum % 2 == 0: - # canvas.showPage() - # # Increment pageNum again since we've added a blank page - # self.__pageNum += 1 - - -def _simulate_br(paragraph_txt: str, para="") -> str: - """Reportlab bug turnaround (could be removed in a future version). - p is a string with Reportlab intra-paragraph XML tags. - Replaces
        (currently ignored by Reportlab) by
        - Also replaces
        by
        - """ - return ("
        " + para).join( - re.split(r"<.*?br.*?/>", paragraph_txt.replace("
        ", "
        ")) - ) - - -def _make_signature_image(signature, leftindent, formsemestre_id) -> Table: - "crée un paragraphe avec l'image signature" - # cree une image PIL pour avoir la taille (W,H) - - f = io.BytesIO(signature) - img = PILImage.open(f) - width, height = img.size - pdfheight = ( - 1.0 - * sco_preferences.get_preference("pv_sig_image_height", formsemestre_id) - * mm - ) - f.seek(0, 0) - - style = styles.ParagraphStyle({}) - style.leading = 1.0 * sco_preferences.get_preference( - "SCOLAR_FONT_SIZE", formsemestre_id - ) # vertical space - style.leftIndent = leftindent - return Table( - [("", Image(f, width=width * pdfheight / float(height), height=pdfheight))], - colWidths=(9 * cm, 7 * cm), - ) - - -def pdf_lettres_individuelles( - formsemestre_id, - etudids=None, - date_jury="", - date_commission="", - signature=None, -): - """Document PDF avec les lettres d'avis pour les etudiants mentionnés - (tous ceux du semestre, ou la liste indiquée par etudids) - Renvoie pdf data ou chaine vide si aucun etudiant avec décision de jury. - """ - dpv = sco_dict_pv_jury.dict_pvjury(formsemestre_id, etudids=etudids, with_prev=True) - if not dpv: - return "" - # Ajoute infos sur etudiants - etuds = [x["identite"] for x in dpv["decisions"]] - sco_etud.fill_etuds_info(etuds) - # - formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id) - prefs = sco_preferences.SemPreferences(formsemestre_id) - params = { - "date_jury": date_jury, - "date_commission": date_commission, - "titre_formation": dpv["formation"]["titre_officiel"], - "htab1": "8cm", # lignes à droite (entete, signature) - "htab2": "1cm", - } - # copie preferences - for name in sco_preferences.get_base_preferences().prefs_name: - params[name] = sco_preferences.get_preference(name, formsemestre_id) - - bookmarks = {} - objects = [] # list of PLATYPUS objects - npages = 0 - for decision in dpv["decisions"]: - if ( - decision["decision_sem"] - or decision.get("decision_annee") - or decision.get("decision_rcue") - ): # decision prise - etud: Identite = Identite.query.get(decision["identite"]["etudid"]) - params["nomEtud"] = etud.nomprenom - bookmarks[npages + 1] = scu.suppress_accents(etud.nomprenom) - try: - objects += pdf_lettre_individuelle( - dpv["formsemestre"], decision, etud, params, signature - ) - except UnidentifiedImageError as exc: - raise ScoValueError( - "Fichier image (signature ou logo ?) invalide !" - ) from exc - objects.append(PageBreak()) - npages += 1 - if npages == 0: - return "" - # Paramètres de mise en page - margins = ( - prefs["left_margin"], - prefs["top_margin"], - prefs["right_margin"], - prefs["bottom_margin"], - ) - - # ----- Build PDF - report = io.BytesIO() # in-memory document, no disk file - document = BaseDocTemplate(report) - document.addPageTemplates( - CourrierIndividuelTemplate( - document, - author=f"{sco_version.SCONAME} {sco_version.SCOVERSION} (E. Viennet)", - title=f"Lettres décision {formsemestre.titre_annee()}", - subject="Décision jury", - margins=margins, - pagesbookmarks=bookmarks, - preferences=prefs, - ) - ) - - document.build(objects) - data = report.getvalue() - return data - - -def _descr_jury(formsemestre: FormSemestre, diplome): - - if not diplome: - if formsemestre.formation.is_apc(): - t = f"""BUT{(formsemestre.semestre_id+1)//2}""" - s = t - else: - t = f"""passage de Semestre {formsemestre.semestre_id} en Semestre {formsemestre.semestre_id + 1}""" - s = "passage de semestre" - else: - t = "délivrance du diplôme" - s = t - return t, s # titre long, titre court - - -def pdf_lettre_individuelle(sem, decision, etud: Identite, params, signature=None): - """ - Renvoie une liste d'objets PLATYPUS pour intégration - dans un autre document. - """ - # - formsemestre_id = sem["formsemestre_id"] - formsemestre = FormSemestre.query.get(formsemestre_id) - Se: SituationEtudCursus = decision["Se"] - t, s = _descr_jury( - formsemestre, Se.parcours_validated() or not Se.semestre_non_terminal - ) - objects = [] - style = reportlab.lib.styles.ParagraphStyle({}) - style.fontSize = 14 - style.fontName = sco_preferences.get_preference("PV_FONTNAME", formsemestre_id) - style.leading = 18 - style.alignment = TA_LEFT - - params["semestre_id"] = formsemestre.semestre_id - params["decision_sem_descr"] = decision["decision_sem_descr"] - params["type_jury"] = t # type de jury (passage ou delivrance) - params["type_jury_abbrv"] = s # idem, abbrégé - params["decisions_ue_descr"] = decision["decisions_ue_descr"] - if decision["decisions_ue_nb"] > 1: - params["decisions_ue_descr_plural"] = "s" - else: - params["decisions_ue_descr_plural"] = "" - - params["INSTITUTION_CITY"] = ( - sco_preferences.get_preference("INSTITUTION_CITY", formsemestre_id) or "" - ) - - if decision["prev_decision_sem"]: - params["prev_semestre_id"] = decision["prev"]["semestre_id"] - - params["prev_decision_sem_txt"] = "" - params["decision_orig"] = "" - - params.update(decision["identite"]) - # fix domicile - if params["domicile"]: - params["domicile"] = params["domicile"].replace("\\n", "
        ") - - # UE capitalisées: - if decision["decisions_ue"] and decision["decisions_ue_descr"]: - params["decision_ue_txt"] = ( - """Unité%(decisions_ue_descr_plural)s d'Enseignement %(decision_orig)s capitalisée%(decisions_ue_descr_plural)s : %(decisions_ue_descr)s""" - % params - ) - else: - params["decision_ue_txt"] = "" - # Mention - params["mention"] = decision["mention"] - # Informations sur compensations - if decision["observation"]: - params["observation_txt"] = ( - """Observation : %(observation)s.""" % decision - ) - else: - params["observation_txt"] = "" - # Autorisations de passage - if decision["autorisations"] and not Se.parcours_validated(): - if len(decision["autorisations"]) > 1: - s = "s" - else: - s = "" - params[ - "autorisations_txt" - ] = """Vous êtes autorisé%s à continuer dans le%s semestre%s : %s""" % ( - etud.e, - s, - s, - decision["autorisations_descr"], - ) - else: - params["autorisations_txt"] = "" - - if decision["decision_sem"] and Se.parcours_validated(): - params["diplome_txt"] = ( - """Vous avez donc obtenu le diplôme : %(titre_formation)s""" % params - ) - else: - params["diplome_txt"] = "" - - # Les fonctions ci-dessous ajoutent ou modifient des champs: - if formsemestre.formation.is_apc(): - # ajout champs spécifiques PV BUT - add_apc_infos(formsemestre, params, decision) - else: - # ajout champs spécifiques PV DUT - add_classic_infos(formsemestre, params, decision) - - # Corps de la lettre: - objects += sco_bulletins_pdf.process_field( - sco_preferences.get_preference("PV_LETTER_TEMPLATE", sem["formsemestre_id"]), - params, - style, - suppress_empty_pars=True, - ) - - # Signature: - # nota: si semestre terminal, signature par directeur IUT, sinon, signature par - # chef de département. - if Se.semestre_non_terminal: - sig = ( - sco_preferences.get_preference( - "PV_LETTER_PASSAGE_SIGNATURE", formsemestre_id - ) - or "" - ) % params - sig = _simulate_br(sig, '') - objects += sco_pdf.make_paras( - ( - """""" - + sig - + """""" - ) - % params, - style, - ) - else: - sig = ( - sco_preferences.get_preference( - "PV_LETTER_DIPLOMA_SIGNATURE", formsemestre_id - ) - or "" - ) % params - sig = _simulate_br(sig, '') - objects += sco_pdf.make_paras( - ( - """""" - + sig - + """""" - ) - % params, - style, - ) - - if signature: - try: - objects.append( - _make_signature_image(signature, params["htab1"], formsemestre_id) - ) - except UnidentifiedImageError as exc: - raise ScoValueError("Image signature invalide !") from exc - - return objects - - -def add_classic_infos(formsemestre: FormSemestre, params: dict, decision: dict): - """Ajoute les champs pour les formations classiques, donc avec codes semestres""" - if decision["prev_decision_sem"]: - params["prev_code_descr"] = decision["prev_code_descr"] - params[ - "prev_decision_sem_txt" - ] = f"""Décision du semestre antérieur S{params['prev_semestre_id']} : {params['prev_code_descr']}""" - # Décision semestre courant: - if formsemestre.semestre_id >= 0: - params["decision_orig"] = f"du semestre S{formsemestre.semestre_id}" - else: - params["decision_orig"] = "" - - -def add_apc_infos(formsemestre: FormSemestre, params: dict, decision: dict): - """Ajoute les champs pour les formations APC (BUT), donc avec codes RCUE et année""" - annee_but = (formsemestre.semestre_id + 1) // 2 - params["decision_orig"] = f"année BUT{annee_but}" - if decision is None: - params["decision_sem_descr"] = "" - params["decision_ue_txt"] = "" - else: - decision_annee = decision.get("decision_annee") or {} - params["decision_sem_descr"] = decision_annee.get("code") or "" - params[ - "decision_ue_txt" - ] = f"""{params["decision_ue_txt"]}
        - Niveaux de compétences:
        {decision.get("descr_decisions_rcue") or ""} - """ - - -# ---------------------------------------------- -def pvjury_pdf( - formsemestre: FormSemestre, - etudids: list[int], - date_commission=None, - date_jury=None, - numero_arrete=None, - code_vdi=None, - show_title=False, - pv_title=None, - with_paragraph_nom=False, - anonymous=False, -) -> bytes: - """Doc PDF récapitulant les décisions de jury - (tableau en format paysage) - """ - objects, a_diplome = _pvjury_pdf_type( - formsemestre, - etudids, - only_diplome=False, - date_commission=date_commission, - numero_arrete=numero_arrete, - code_vdi=code_vdi, - date_jury=date_jury, - show_title=show_title, - pv_title=pv_title, - with_paragraph_nom=with_paragraph_nom, - anonymous=anonymous, - ) - if not objects: - return b"" - - jury_de_diplome = formsemestre.est_terminal() - - # Si Jury de passage et qu'un étudiant valide le parcours (car il a validé antérieurement le dernier semestre) - # alors on génère aussi un PV de diplome (à la suite dans le même doc PDF) - if not jury_de_diplome and a_diplome: - # au moins un etudiant a validé son diplome: - objects.append(PageBreak()) - objects += _pvjury_pdf_type( - formsemestre, - etudids, - only_diplome=True, - date_commission=date_commission, - date_jury=date_jury, - numero_arrete=numero_arrete, - code_vdi=code_vdi, - show_title=show_title, - pv_title=pv_title, - with_paragraph_nom=with_paragraph_nom, - anonymous=anonymous, - )[0] - - # ----- Build PDF - report = io.BytesIO() # in-memory document, no disk file - document = BaseDocTemplate(report) - document.pagesize = landscape(A4) - document.addPageTemplates( - PVTemplate( - document, - author=f"{sco_version.SCONAME} {sco_version.SCOVERSION} (E. Viennet)", - title=SU(f"PV du jury de {formsemestre.titre_num()}"), - subject="PV jury", - preferences=sco_preferences.SemPreferences(formsemestre.id), - ) - ) - - document.build(objects) - data = report.getvalue() - return data - - -def _pvjury_pdf_type( - formsemestre: FormSemestre, - etudids: list[int], - only_diplome=False, - date_commission=None, - date_jury=None, - numeroArrete=None, - VDICode=None, - showTitle=False, - pv_title=None, - anonymous=False, - with_paragraph_nom=False, -) -> tuple[list, bool]: - """Objets platypus PDF récapitulant les décisions de jury - pour un type de jury (passage ou delivrance). - Ramene: liste d'onj platypus, et un boolen indiquant si au moins un étudiant est diplômé. - """ - from app.scodoc import sco_pvjury - - a_diplome = False - # Jury de diplome si sem. terminal OU que l'on demande seulement les diplomés - diplome = formsemestre.est_terminal() or only_diplome - titre_jury, _ = _descr_jury(formsemestre, diplome) - titre_diplome = pv_title or formsemestre.formation.titre_officiel - objects = [] - - style = reportlab.lib.styles.ParagraphStyle({}) - style.fontSize = 12 - style.fontName = sco_preferences.get_preference("PV_FONTNAME", formsemestre_id) - style.leading = 18 - style.alignment = TA_JUSTIFY - - indent = 1 * cm - bulletStyle = reportlab.lib.styles.ParagraphStyle({}) - bulletStyle.fontSize = 12 - bulletStyle.fontName = sco_preferences.get_preference( - "PV_FONTNAME", formsemestre_id - ) - bulletStyle.leading = 12 - bulletStyle.alignment = TA_JUSTIFY - bulletStyle.firstLineIndent = 0 - bulletStyle.leftIndent = indent - bulletStyle.bulletIndent = indent - bulletStyle.bulletFontName = "Times-Roman" - bulletStyle.bulletFontSize = 11 - bulletStyle.spaceBefore = 5 * mm - bulletStyle.spaceAfter = 5 * mm - - objects += [Spacer(0, 5 * mm)] - objects += sco_pdf.make_paras( - f""" - Procès-verbal de {titre_jury} du département { - sco_preferences.get_preference("DeptName", formsemestre.id) or "(sans nom)" - } - Session unique {formsemestre.annee_scolaire()} - """, - style, - ) - - objects += sco_pdf.make_paras( - """ - %s - """ - % titre_diplome, - style, - ) - - if showTitle: - objects += sco_pdf.make_paras( - """Semestre: %s""" % sem["titre"], style - ) - if sco_preferences.get_preference("PV_TITLE_WITH_VDI", formsemestre.id): - objects += sco_pdf.make_paras( - """VDI et Code: %s""" % (VDICode or ""), style - ) - - if date_jury: - objects += sco_pdf.make_paras( - """Jury tenu le %s""" % date_jury, style - ) - - objects += sco_pdf.make_paras( - "" - + (sco_preferences.get_preference("PV_INTRO", formsemestre.id) or "") - % { - "Decnum": numero_arrete, - "VDICode": code_vdi, - "UnivName": sco_preferences.get_preference("UnivName", formsemestre.id), - "Type": titre_jury, - "Date": date_commission, # deprecated - "date_commission": date_commission, - } - + "", - bulletStyle, - ) - - objects += sco_pdf.make_paras( - """Le jury propose les décisions suivantes :""", style - ) - objects += [Spacer(0, 4 * mm)] - - if formsemestre.formation.is_apc(): - rows, titles = jury_but_pv.pvjury_table_but( - formsemestre, etudids=etudids, line_sep="
        " - ) - columns_ids = list(titles.keys()) - a_diplome = codes_cursus.ADM in [row.get("diplome") for row in rows] - else: - dpv = sco_dict_pv_jury.dict_pvjury( - formsemestre.id, etudids=etudids, with_prev=True - ) - if not dpv: - return [], False - rows, titles, columns_ids = sco_pvjury.pvjury_table( - dpv, - only_diplome=only_diplome, - anonymous=anonymous, - with_paragraph_nom=with_paragraph_nom, - ) - a_diplome = True in (x["validation_parcours"] for x in dpv["decisions"]) - # convert to lists of tuples: - columns_ids = ["etudid"] + columns_ids - lines = [[line.get(x, "") for x in columns_ids] for line in lines] - titles = [titles.get(x, "") for x in columns_ids] - # Make a new cell style and put all cells in paragraphs - cell_style = styles.ParagraphStyle({}) - cell_style.fontSize = sco_preferences.get_preference( - "SCOLAR_FONT_SIZE", formsemestre.id - ) - cell_style.fontName = sco_preferences.get_preference("PV_FONTNAME", formsemestre.id) - cell_style.leading = 1.0 * sco_preferences.get_preference( - "SCOLAR_FONT_SIZE", formsemestre.id - ) # vertical space - LINEWIDTH = 0.5 - table_style = [ - ( - "FONTNAME", - (0, 0), - (-1, 0), - sco_preferences.get_preference("PV_FONTNAME", formsemestre.id), - ), - ("LINEBELOW", (0, 0), (-1, 0), LINEWIDTH, Color(0, 0, 0)), - ("GRID", (0, 0), (-1, -1), LINEWIDTH, Color(0, 0, 0)), - ("VALIGN", (0, 0), (-1, -1), "TOP"), - ] - titles = ["%s" % x for x in titles] - - def _format_pv_cell(x): - """convert string to paragraph""" - if isinstance(x, str): - return Paragraph(SU(x), cell_style) - else: - return x - - widths_by_id = { - "nom": 5 * cm, - "cursus": 2.8 * cm, - "ects": 1.4 * cm, - "devenir": 1.8 * cm, - "decision_but": 1.8 * cm, - } - - table_cells = [[_format_pv_cell(x) for x in line[1:]] for line in ([titles] + rows)] - widths = [widths_by_id.get(col_id) for col_id in columns_ids[1:]] - - objects.append( - Table(table_cells, repeatRows=1, colWidths=widths, style=table_style) - ) - - # Signature du directeur - objects += sco_pdf.make_paras( - f"""{ - sco_preferences.get_preference("DirectorName", formsemestre.id) or "" - }, { - sco_preferences.get_preference("DirectorTitle", formsemestre.id) or "" - }""", - style, - ) - - # Légende des codes - codes = list(codes_cursus.CODES_EXPL.keys()) - codes.sort() - objects += sco_pdf.make_paras( - """ - Codes utilisés :""", - style, - ) - L = [] - for code in codes: - L.append((code, codes_cursus.CODES_EXPL[code])) - TableStyle2 = [ - ( - "FONTNAME", - (0, 0), - (-1, 0), - sco_preferences.get_preference("PV_FONTNAME", formsemestre.id), - ), - ("LINEBELOW", (0, 0), (-1, -1), LINEWIDTH, Color(0, 0, 0)), - ("LINEABOVE", (0, 0), (-1, -1), LINEWIDTH, Color(0, 0, 0)), - ("LINEBEFORE", (0, 0), (0, -1), LINEWIDTH, Color(0, 0, 0)), - ("LINEAFTER", (-1, 0), (-1, -1), LINEWIDTH, Color(0, 0, 0)), - ] - objects.append( - Table( - [[Paragraph(SU(x), cell_style) for x in line] for line in L], - colWidths=(2 * cm, None), - style=TableStyle2, - ) - ) - - return objects, a_diplome diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py index b70ad701..af91313f 100644 --- a/app/scodoc/sco_recapcomplet.py +++ b/app/scodoc/sco_recapcomplet.py @@ -169,13 +169,14 @@ def formsemestre_recapcomplet( if len(formsemestre.inscriptions) > 0: H.append("""") if sco_preferences.get_preference("use_ue_coefs", formsemestre_id): @@ -431,13 +448,15 @@ def gen_formsemestre_recapcomplet_html_table( """ table = None table_html = None - if not (mode_jury or selected_etudid): - if include_evaluations: - table_html = sco_cache.TableRecapWithEvalsCache.get(formsemestre.id) - else: - table_html = sco_cache.TableRecapCache.get(formsemestre.id) - # en mode jury ne cache pas la table html - if mode_jury or (table_html is None): + cache_class = { + (True, True): sco_cache.TableJuryWithEvalsCache, + (True, False): sco_cache.TableJuryCache, + (False, True): sco_cache.TableRecapWithEvalsCache, + (False, False): sco_cache.TableRecapCache, + }[(bool(mode_jury), bool(include_evaluations))] + if not selected_etudid: + table_html = cache_class.get(formsemestre.id) + if table_html is None: table = _gen_formsemestre_recapcomplet_table( res, include_evaluations, @@ -446,11 +465,7 @@ def gen_formsemestre_recapcomplet_html_table( selected_etudid=selected_etudid, ) table_html = table.html() - if not mode_jury: - if include_evaluations: - sco_cache.TableRecapWithEvalsCache.set(formsemestre.id, table_html) - else: - sco_cache.TableRecapCache.set(formsemestre.id, table_html) + cache_class.set(formsemestre.id, table_html) return table_html, table @@ -472,6 +487,7 @@ def _gen_formsemestre_recapcomplet_table( mode_jury=mode_jury, read_only=not res.formsemestre.can_edit_jury(), ) + table.data["filename"] = filename table.select_row(selected_etudid) return table diff --git a/app/scodoc/sco_report.py b/app/scodoc/sco_report.py index 178cf686..70bc7afe 100644 --- a/app/scodoc/sco_report.py +++ b/app/scodoc/sco_report.py @@ -1574,7 +1574,8 @@ def formsemestre_graph_cursus( allkeys=False, # unused ): """Graphe suivi cohortes""" - annee_bac = str(annee_bac) + annee_bac = str(annee_bac or "") + annee_admission = str(annee_admission or "") # log("formsemestre_graph_cursus") sem = sco_formsemestre.get_formsemestre(formsemestre_id) if format == "pdf": diff --git a/app/scodoc/table_builder.py b/app/scodoc/table_builder.py deleted file mode 100644 index 8ddd70c8..00000000 --- a/app/scodoc/table_builder.py +++ /dev/null @@ -1,401 +0,0 @@ -############################################################################## -# ScoDoc -# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. -# See LICENSE -############################################################################## - -"""Classes pour aider à construire des tables de résultats -""" -from collections import defaultdict - - -class Element: - def __init__( - self, - elt: str, - content=None, - classes: list[str] = None, - attrs: dict[str, str] = None, - data: dict = None, - ): - self.elt = elt - self.attrs = attrs or {} - self.classes = classes or [] - "list of classes for the element" - self.content = content - self.data = data or {} - "data-xxx" - - def html(self, extra_classes: list[str] = None) -> str: - "html for element" - classes = [cls for cls in (self.classes + (extra_classes or [])) if cls] - attrs_str = f"""class="{' '.join(classes)}" """ if classes else "" - # Autres attributs: - attrs_str += " " + " ".join([f'{k}="{v}"' for (k, v) in self.attrs.items()]) - # et data-x - attrs_str += " " + " ".join([f'data-{k}="{v}"' for k, v in self.data.items()]) - return f"""<{self.elt} {attrs_str}>{self.html_content()}""" - - def html_content(self) -> str: - "Le contenu de l'élément, en html." - return str(self.content or "") - - -class Table(Element): - """Construction d'une table de résultats - - table = Table() - row = table.new_row(id="xxx", category="yyy") - row.new_cell( col_id, title, content [,classes] [, idx], [group], [keys:dict={}] ) - - rows = table.get_rows([category="yyy"]) - table.sort_rows(key [, reverse]) - table.set_titles(titles) - table.update_titles(titles) - - table.set_column_groups(groups: list[str]) - table.insert_group(group:str, [after=str], [before=str]) - - Ordre des colonnes: groupées par groupes, et dans chaque groupe par ordre d'insertion - On fixe l'ordre des groupes par ordre d'insertion - ou par insert_group ou par set_column_groups. - - """ - - def __init__( - self, - selected_row_id: str = None, - classes: list[str] = None, - attrs: dict[str, str] = None, - data: dict = None, - ): - super().__init__("table", classes=classes, attrs=attrs, data=data) - self.rows: list["Row"] = [] - "ordered list of Rows" - self.row_by_id: dict[str, "Row"] = {} - self.column_ids = [] - "ordered list of columns ids" - self.groups = [] - "ordered list of column groups names" - self.head = [] - self.foot = [] - self.column_group = {} - "the group of the column: { col_id : group }" - self.column_classes: defaultdict[str, list[str]] = defaultdict(lambda: []) - "classe ajoutée à toutes les cellules de la colonne: { col_id : class }" - self.selected_row_id = selected_row_id - "l'id de la ligne sélectionnée" - self.titles = {} - "Column title: { col_id : titre }" - self.head_title_row: "Row" = Row(self, "title_head", cell_elt="th") - self.foot_title_row: "Row" = Row(self, "title_foot", cell_elt="th") - self.empty_cell = Cell.empty() - - def _prepare(self): - """Prepare the table before generation: - Sort table columns, add header/footer titles rows - """ - self.sort_columns() - # Titres - self.add_head_row(self.head_title_row) - self.add_foot_row(self.foot_title_row) - - def get_row_by_id(self, row_id) -> "Row": - "return the row, or None" - return self.row_by_id.get(row_id) - - def is_empty(self) -> bool: - "true if table has no rows" - return len(self.rows) == 0 - - def select_row(self, row_id): - "mark rows as 'selected'" - self.selected_row_id = row_id - - def to_list(self) -> list[dict]: - """as a list, each row is a dict""" - self._prepare() - return [row.to_dict() for row in self.rows] - - def html(self, extra_classes: list[str] = None) -> str: - """HTML version of the table""" - self._prepare() - return super().html(extra_classes=extra_classes) - - def html_content(self) -> str: - """Le contenu de la table en html.""" - newline = "\n" - header = ( - f""" -