From c8a70670beded75c9eacc26833ece3a6d5dddc3d Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 19 Feb 2023 02:54:29 +0100 Subject: [PATCH] =?UTF-8?q?PV=20Jury=20PDF:=20refactoring,=20optimisation,?= =?UTF-8?q?=20am=C3=A9lioration.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/jury_but_pv.py | 9 +- app/but/jury_but_results.py | 4 +- app/scodoc/sco_archives.py | 48 ++-- app/scodoc/sco_bulletins.py | 4 +- app/scodoc/sco_dict_pv_jury.py | 324 ++++++++++++++++++++++ app/scodoc/sco_export_results.py | 8 +- app/scodoc/sco_formsemestre_validation.py | 4 +- app/scodoc/sco_inscr_passage.py | 4 +- app/scodoc/sco_pv_forms.py | 292 +------------------ app/scodoc/sco_pvpdf.py | 190 +++++++------ 10 files changed, 472 insertions(+), 415 deletions(-) create mode 100644 app/scodoc/sco_dict_pv_jury.py diff --git a/app/but/jury_but_pv.py b/app/but/jury_but_pv.py index 551c4d3a..9d177b0e 100644 --- a/app/but/jury_but_pv.py +++ b/app/but/jury_but_pv.py @@ -92,17 +92,12 @@ def pvjury_page_but(formsemestre_id: int, fmt="html"): def pvjury_table_but( - formsemestre: FormSemestre, - etudids: list[int] = None, - line_sep: str = "\n", - only_diplome=False, - anonymous=False, - with_paragraph_nom=False, + formsemestre: FormSemestre, etudids: list[int] = None, line_sep: str = "\n" ) -> 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_pv_forms.pvjury_table + # remplace pour le BUT la fonction sco_pvjury.pvjury_table annee_but = (formsemestre.semestre_id + 1) // 2 titles = { "nom": "Code" if anonymous else "Nom", diff --git a/app/but/jury_but_results.py b/app/but/jury_but_results.py index f6a208f3..d947bf84 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_pv_dict +from app.scodoc import sco_dict_pv_jury 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_pv_dict.dict_pvjury(formsemestre.id) + dpv = sco_dict_pv_jury.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/scodoc/sco_archives.py b/app/scodoc/sco_archives.py index fb797275..3064f1a2 100644 --- a/app/scodoc/sco_archives.py +++ b/app/scodoc/sco_archives.py @@ -71,14 +71,14 @@ from app.comp import res_sem from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre from app.scodoc.TrivialFormulator import TrivialFormulator -from app.scodoc.sco_exceptions import AccessDenied, ScoPermissionDenied +from app.scodoc.sco_exceptions import ScoPermissionDenied 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_pv_forms -from app.scodoc import sco_pv_lettres_inviduelles -from app.scodoc import sco_pv_pdf +from app.scodoc import sco_pvjury +from app.scodoc import sco_dict_pv_jury +from app.scodoc import sco_pvpdf from app.scodoc.sco_exceptions import ScoValueError @@ -395,23 +395,27 @@ def do_formsemestre_archive( signature=signature, ) if data: - PVArchive.store(archive_id, f"CourriersDecisions{groups_filename}.pdf", data) + PVArchive.store(archive_id, "CourriersDecisions%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) + # 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) def formsemestre_archive(formsemestre_id, group_ids: list[int] = None): @@ -427,8 +431,6 @@ def formsemestre_archive(formsemestre_id, group_ids: list[int] = None): formsemestre_id=formsemestre_id, ) ) - - sem = sco_formsemestre.get_formsemestre(formsemestre_id) if not group_ids: # tous les inscrits du semestre group_ids = [sco_groups.get_default_group(formsemestre_id)] @@ -469,7 +471,7 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement. ), ("sep", {"input_type": "separator", "title": "Informations sur PV de jury"}), ] - descr += sco_pv_forms.descrform_pvjury(formsemestre) + descr += sco_pvjury.descrform_pvjury(formsemestre) descr += [ ( "signature", diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index 7230bffa..83c10a10 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -59,7 +59,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_pv_dict +from app.scodoc import sco_dict_pv_jury from app.scodoc import sco_users import app.scodoc.sco_utils as scu from app.scodoc.sco_utils import ModuleType, fmt_note @@ -787,7 +787,7 @@ def etud_descr_situation_semestre( infos["date_defaillance"] = date_def infos["descr_decision_jury"] = f"Défaillant{ne}" - dpv = sco_pv_dict.dict_pvjury(formsemestre_id, etudids=[etudid]) + dpv = sco_dict_pv_jury.dict_pvjury(formsemestre_id, etudids=[etudid]) if dpv: infos["decision_sem"] = dpv["decisions"][0]["decision_sem"] diff --git a/app/scodoc/sco_dict_pv_jury.py b/app/scodoc/sco_dict_pv_jury.py new file mode 100644 index 00000000..1075a2ce --- /dev/null +++ b/app/scodoc/sco_dict_pv_jury.py @@ -0,0 +1,324 @@ +# -*- 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 +# +############################################################################## + +"""Ancienne fonction de synthèse des information jury + (pour formations classiques) +""" +from operator import itemgetter + +from app import log +from app.comp import res_sem +from app.comp.res_compat import NotesTableCompat +from app.models import ( + Formation, + FormSemestre, + Identite, + ScolarAutorisationInscription, + UniteEns, + but_validations, +) +from app.scodoc import codes_cursus +from app.scodoc import sco_etud +from app.scodoc import sco_formsemestre +from app.scodoc import sco_cursus +from app.scodoc import sco_cursus_dut +import app.scodoc.notesdb as ndb +import app.scodoc.sco_utils as scu + + +def dict_pvjury( + formsemestre_id, + etudids=None, + with_prev=False, + with_parcours_decisions=False, +): + """Données pour édition jury + etudids == None => tous les inscrits, sinon donne la liste des ids + Si with_prev: ajoute infos sur code jury semestre precedent + Si with_parcours_decisions: ajoute infos sur code decision jury de tous les semestre du parcours + Résultat: + { + 'date' : date de la decision la plus recente, + 'formsemestre' : sem, + 'is_apc' : bool, + 'formation' : { 'acronyme' :, 'titre': ... } + 'decisions' : { [ { 'identite' : {'nom' :, 'prenom':, ...,}, + 'etat' : I ou D ou DEF + 'decision_sem' : {'code':, 'code_prev': }, + 'decisions_ue' : { ue_id : { 'code' : ADM|CMP|AJ, 'event_date' :, + 'acronyme', 'numero': } }, + 'autorisations' : [ { 'semestre_id' : { ... } } ], + 'validation_parcours' : True si parcours validé (diplome obtenu) + 'prev_code' : code (calculé slt si with_prev), + 'mention' : mention (en fct moy gen), + 'sum_ects' : total ECTS acquis dans ce semestre (incluant les UE capitalisées) + 'sum_ects_capitalises' : somme des ECTS des UE capitalisees + } + ] + }, + 'decisions_dict' : { etudid : decision (comme ci-dessus) }, + } + """ + formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) + nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) + if etudids is None: + etudids = nt.get_etudids() + if not etudids: + return {} + cnx = ndb.GetDBConnexion() + sem = sco_formsemestre.get_formsemestre(formsemestre_id) + max_date = "0000-01-01" + has_prev = False # vrai si au moins un etudiant a un code prev + semestre_non_terminal = False # True si au moins un etudiant a un devenir + + decisions = [] + D = {} # même chose que decisions, mais { etudid : dec } + for etudid in etudids: + etud: Identite = Identite.query.get(etudid) + Se = sco_cursus.get_situation_etud_cursus( + etud.to_dict_scodoc7(), formsemestre_id + ) + semestre_non_terminal = semestre_non_terminal or Se.semestre_non_terminal + d = {} + d["identite"] = nt.identdict[etudid] + d["etat"] = nt.get_etud_etat( + etudid + ) # I|D|DEF (inscription ou démission ou défaillant) + d["decision_sem"] = nt.get_etud_decision_sem(etudid) + d["decisions_ue"] = nt.get_etud_decision_ues(etudid) + if formsemestre.formation.is_apc(): + d.update(but_validations.dict_decision_jury(etud, formsemestre)) + d["last_formsemestre_id"] = Se.get_semestres()[ + -1 + ] # id du dernier semestre (chronologiquement) dans lequel il a été inscrit + + ects_capitalises_by_ue_code = _comp_ects_capitalises_by_ue_code(nt, etudid) + d["sum_ects_capitalises"] = sum(ects_capitalises_by_ue_code.values()) + ects_by_ue_code = _comp_ects_by_ue_code(nt, d["decisions_ue"]) + d["sum_ects"] = _sum_ects_dicts(ects_capitalises_by_ue_code, ects_by_ue_code) + + if d["decision_sem"] and codes_cursus.code_semestre_validant( + d["decision_sem"]["code"] + ): + d["mention"] = scu.get_mention(nt.get_etud_moy_gen(etudid)) + else: + d["mention"] = "" + # Versions "en français": (avec les UE capitalisées d'ailleurs) + dec_ue_list = _descr_decisions_ues( + nt, etudid, d["decisions_ue"], d["decision_sem"] + ) + d["decisions_ue_nb"] = len( + dec_ue_list + ) # avec les UE capitalisées, donc des éventuels doublons + # Mais sur la description (eg sur les bulletins), on ne veut pas + # afficher ces doublons: on uniquifie sur ue_code + _codes = set() + ue_uniq = [] + for ue in dec_ue_list: + if ue["ue_code"] not in _codes: + ue_uniq.append(ue) + _codes.add(ue["ue_code"]) + + d["decisions_ue_descr"] = ", ".join([ue["acronyme"] for ue in ue_uniq]) + if nt.is_apc: + d["decision_sem_descr"] = "" # pas de validation de semestre en BUT + else: + d["decision_sem_descr"] = _descr_decision_sem(d["etat"], d["decision_sem"]) + + autorisations = ScolarAutorisationInscription.query.filter_by( + etudid=etudid, origin_formsemestre_id=formsemestre_id + ).all() + d["autorisations"] = [a.to_dict() for a in autorisations] + d["autorisations_descr"] = _descr_autorisations(autorisations) + + d["validation_parcours"] = Se.parcours_validated() + d["parcours"] = Se.get_cursus_descr(filter_futur=True) + if with_parcours_decisions: + d["parcours_decisions"] = Se.get_parcours_decisions() + # Observations sur les compensations: + compensators = sco_cursus_dut.scolar_formsemestre_validation_list( + cnx, args={"compense_formsemestre_id": formsemestre_id, "etudid": etudid} + ) + obs = [] + for compensator in compensators: + # nb: il ne devrait y en avoir qu'un ! + csem = sco_formsemestre.get_formsemestre(compensator["formsemestre_id"]) + obs.append( + "%s compensé par %s (%s)" + % (sem["sem_id_txt"], csem["sem_id_txt"], csem["anneescolaire"]) + ) + + if d["decision_sem"] and d["decision_sem"]["compense_formsemestre_id"]: + compensed = sco_formsemestre.get_formsemestre( + d["decision_sem"]["compense_formsemestre_id"] + ) + obs.append( + f"""{sem["sem_id_txt"]} compense {compensed["sem_id_txt"]} ({compensed["anneescolaire"]})""" + ) + + d["observation"] = ", ".join(obs) + + # Cherche la date de decision (sem ou UE) la plus récente: + if d["decision_sem"]: + date = ndb.DateDMYtoISO(d["decision_sem"]["event_date"]) + if date and date > max_date: # decision plus recente + max_date = date + if d["decisions_ue"]: + for dec_ue in d["decisions_ue"].values(): + if dec_ue: + date = ndb.DateDMYtoISO(dec_ue["event_date"]) + if date and date > max_date: # decision plus recente + max_date = date + # Code semestre precedent + if with_prev: # optionnel car un peu long... + info = sco_etud.get_etud_info(etudid=etudid, filled=True) + if not info: + continue # should not occur + etud = info[0] + if Se.prev and Se.prev_decision: + d["prev_decision_sem"] = Se.prev_decision + d["prev_code"] = Se.prev_decision["code"] + d["prev_code_descr"] = _descr_decision_sem( + scu.INSCRIT, Se.prev_decision + ) + d["prev"] = Se.prev + has_prev = True + else: + d["prev_decision_sem"] = None + d["prev_code"] = "" + d["prev_code_descr"] = "" + d["Se"] = Se + + decisions.append(d) + D[etudid] = d + + return { + "date": ndb.DateISOtoDMY(max_date), + "formsemestre": sem, + "is_apc": nt.is_apc, + "has_prev": has_prev, + "semestre_non_terminal": semestre_non_terminal, + "formation": Formation.query.get_or_404(sem["formation_id"]).to_dict(), + "decisions": decisions, + "decisions_dict": D, + } + + +def _comp_ects_capitalises_by_ue_code(nt: NotesTableCompat, etudid: int): + """Calcul somme des ECTS des UE capitalisees""" + ues = nt.get_ues_stat_dict() + ects_by_ue_code = {} + for ue in ues: + ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"]) + if ue_status and ue_status["is_capitalized"]: + ects_val = float(ue_status["ue"]["ects"] or 0.0) + ects_by_ue_code[ue["ue_code"]] = ects_val + + return ects_by_ue_code + + +def _comp_ects_by_ue_code(nt, decision_ues): + """Calcul somme des ECTS validés dans ce semestre (sans les UE capitalisées) + decision_ues est le resultat de nt.get_etud_decision_ues + Chaque resultat est un dict: { ue_code : ects } + """ + if not decision_ues: + return {} + + ects_by_ue_code = {} + for ue_id in decision_ues: + d = decision_ues[ue_id] + ue = UniteEns.query.get(ue_id) + ects_by_ue_code[ue.ue_code] = d["ects"] + + return ects_by_ue_code + + +def _descr_autorisations(autorisations: list[ScolarAutorisationInscription]) -> str: + "résumé textuel des autorisations d'inscription (-> 'S1, S3' )" + return ", ".join([f"S{a.semestre_id}" for a in autorisations]) + + +def _descr_decisions_ues(nt, etudid, decisions_ue, decision_sem) -> list[dict]: + """Liste des UE validées dans ce semestre (incluant les UE capitalisées)""" + if not decisions_ue: + return [] + uelist = [] + # Les UE validées dans ce semestre: + for ue_id in decisions_ue.keys(): + try: + if decisions_ue[ue_id] and ( + codes_cursus.code_ue_validant(decisions_ue[ue_id]["code"]) + or ( + (not nt.is_apc) + and ( + # XXX ceci devrait dépendre du parcours et non pas être une option ! #sco8 + decision_sem + and scu.CONFIG.CAPITALIZE_ALL_UES + and codes_cursus.code_semestre_validant(decision_sem["code"]) + ) + ) + ): + ue = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0] + uelist.append(ue) + except: + log( + f"Exception in descr_decisions_ues: ue_id={ue_id} decisions_ue={decisions_ue}" + ) + # Les UE capitalisées dans d'autres semestres: + if etudid in nt.validations.ue_capitalisees.index: + for ue_id in nt.validations.ue_capitalisees.loc[[etudid]]["ue_id"]: + try: + uelist.append(nt.get_etud_ue_status(etudid, ue_id)["ue"]) + except (KeyError, TypeError): + pass + uelist.sort(key=itemgetter("numero")) + + return uelist + + +def _descr_decision_sem(etat, decision_sem): + "résumé textuel de la décision de semestre" + if etat == "D": + decision = "Démission" + else: + if decision_sem: + cod = decision_sem["code"] + decision = codes_cursus.CODES_EXPL.get(cod, "") # + ' (%s)' % cod + else: + decision = "" + return decision + + +def _sum_ects_dicts(s, t): + """Somme deux dictionnaires { ue_code : ects }, + quand une UE de même code apparait deux fois, prend celle avec le plus d'ECTS. + """ + sum_ects = sum(s.values()) + sum(t.values()) + for ue_code in set(s).intersection(set(t)): + sum_ects -= min(s[ue_code], t[ue_code]) + return sum_ects diff --git a/app/scodoc/sco_export_results.py b/app/scodoc/sco_export_results.py index 9b01e259..dc0448f8 100644 --- a/app/scodoc/sco_export_results.py +++ b/app/scodoc/sco_export_results.py @@ -39,10 +39,8 @@ from app.models import Formation from app.scodoc import html_sco_header from app.scodoc import sco_bac from app.scodoc import codes_cursus -from app.scodoc import sco_cache -from app.scodoc import sco_formations from app.scodoc import sco_preferences -from app.scodoc import sco_pv_dict +from app.scodoc import sco_dict_pv_jury from app.scodoc import sco_etud import sco_version from app.scodoc.gen_tables import GenTable @@ -59,7 +57,7 @@ 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_pv_dict.dict_pvjury( + dpv_by_sem[formsemestre_id] = sco_dict_pv_jury.dict_pvjury( formsemestre_id, with_parcours_decisions=True ) @@ -352,7 +350,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_pv_dict.dict_pvjury( formsemestre_id, with_parcours_decisions=True) + dpv_by_sem[formsemestre_id] = sco_dict_pv_jury.dict_pvjury( formsemestre_id, with_parcours_decisions=True) semlist = [ dpv['formsemestre'] for dpv in dpv_by_sem.values() ] diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py index c5806fd2..49ca5de0 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_pv_dict +from app.scodoc import sco_dict_pv_jury # ------------------------------------------------------------------------------------ def formsemestre_validation_etud_form( @@ -562,7 +562,7 @@ def formsemestre_recap_parcours_table( is_cur = Se.formsemestre_id == sem["formsemestre_id"] num_sem += 1 - dpv = sco_pv_dict.dict_pvjury(sem["formsemestre_id"], etudids=[etudid]) + dpv = sco_dict_pv_jury.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_inscr_passage.py b/app/scodoc/sco_inscr_passage.py index 0fa761a6..eafb63cc 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_pv_dict +from app.scodoc import sco_dict_pv_jury 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_pv_dict.dict_pvjury(src["formsemestre_id"]) + dpv = sco_dict_pv_jury.dict_pvjury(src["formsemestre_id"]) if not dpv: return [] etuds = [ diff --git a/app/scodoc/sco_pv_forms.py b/app/scodoc/sco_pv_forms.py index 142110df..e563fc3e 100644 --- a/app/scodoc/sco_pv_forms.py +++ b/app/scodoc/sco_pv_forms.py @@ -38,15 +38,18 @@ import flask from flask import flash, redirect, url_for from flask import g, request -from app.models import FormSemestre, Identite +from app.models import ( + Formation, + FormSemestre, + ScolarAutorisationInscription, +) +from app.models.etudiants import 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_cursus -from app.scodoc import sco_cursus_dut -from app.scodoc import sco_edit_ue +from app.scodoc import sco_dict_pv_jury from app.scodoc import sco_etud from app.scodoc import sco_groups from app.scodoc import sco_groups_view @@ -60,57 +63,6 @@ from app.scodoc.sco_pdf import PDFLOCK from app.scodoc.TrivialFormulator import TrivialFormulator -def _descr_decisions_ues(nt, etudid, decisions_ue, decision_sem) -> list[dict]: - """Liste des UE validées dans ce semestre (incluant les UE capitalisées)""" - if not decisions_ue: - return [] - uelist = [] - # Les UE validées dans ce semestre: - for ue_id in decisions_ue.keys(): - try: - if decisions_ue[ue_id] and ( - codes_cursus.code_ue_validant(decisions_ue[ue_id]["code"]) - or ( - (not nt.is_apc) - and ( - # XXX ceci devrait dépendre du parcours et non pas être une option ! #sco8 - decision_sem - and scu.CONFIG.CAPITALIZE_ALL_UES - and codes_cursus.code_semestre_validant(decision_sem["code"]) - ) - ) - ): - ue = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0] - uelist.append(ue) - except: - log( - f"Exception in descr_decisions_ues: ue_id={ue_id} decisions_ue={decisions_ue}" - ) - # Les UE capitalisées dans d'autres semestres: - if etudid in nt.validations.ue_capitalisees.index: - for ue_id in nt.validations.ue_capitalisees.loc[[etudid]]["ue_id"]: - try: - uelist.append(nt.get_etud_ue_status(etudid, ue_id)["ue"]) - except (KeyError, TypeError): - pass - uelist.sort(key=itemgetter("numero")) - - return uelist - - -def _descr_decision_sem(etat, decision_sem): - "résumé textuel de la décision de semestre" - if etat == "D": - decision = "Démission" - else: - if decision_sem: - cod = decision_sem["code"] - decision = codes_cursus.CODES_EXPL.get(cod, "") # + ' (%s)' % cod - else: - decision = "" - return decision - - def _descr_decision_sem_abbrev(etat, decision_sem): "résumé textuel tres court (code) de la décision de semestre" if etat == "D": @@ -123,232 +75,6 @@ def _descr_decision_sem_abbrev(etat, decision_sem): return decision -def descr_autorisations(autorisations: list[ScolarAutorisationInscription]) -> str: - "résumé textuel des autorisations d'inscription (-> 'S1, S3' )" - return ", ".join([f"S{a.semestre_id}" for a in autorisations]) - - -def _comp_ects_by_ue_code(nt, decision_ues): - """Calcul somme des ECTS validés dans ce semestre (sans les UE capitalisées) - decision_ues est le resultat de nt.get_etud_decision_ues - Chaque resultat est un dict: { ue_code : ects } - """ - if not decision_ues: - return {} - - ects_by_ue_code = {} - for ue_id in decision_ues: - d = decision_ues[ue_id] - ue = UniteEns.query.get(ue_id) - ects_by_ue_code[ue.ue_code] = d["ects"] - - return ects_by_ue_code - - -def _comp_ects_capitalises_by_ue_code(nt: NotesTableCompat, etudid: int): - """Calcul somme des ECTS des UE capitalisees""" - ues = nt.get_ues_stat_dict() - ects_by_ue_code = {} - for ue in ues: - ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"]) - if ue_status and ue_status["is_capitalized"]: - ects_val = float(ue_status["ue"]["ects"] or 0.0) - ects_by_ue_code[ue["ue_code"]] = ects_val - - return ects_by_ue_code - - -def _sum_ects_dicts(s, t): - """Somme deux dictionnaires { ue_code : ects }, - quand une UE de même code apparait deux fois, prend celle avec le plus d'ECTS. - """ - sum_ects = sum(s.values()) + sum(t.values()) - for ue_code in set(s).intersection(set(t)): - sum_ects -= min(s[ue_code], t[ue_code]) - return sum_ects - - -def dict_pvjury( - formsemestre_id, - etudids=None, - with_prev=False, - with_parcours_decisions=False, -): - """Données pour édition jury - etudids == None => tous les inscrits, sinon donne la liste des ids - Si with_prev: ajoute infos sur code jury semestre precedent - Si with_parcours_decisions: ajoute infos sur code decision jury de tous les semestre du parcours - Résultat: - { - 'date' : date de la decision la plus recente, - 'formsemestre' : sem, - 'is_apc' : bool, - 'formation' : { 'acronyme' :, 'titre': ... } - 'decisions' : { [ { 'identite' : {'nom' :, 'prenom':, ...,}, - 'etat' : I ou D ou DEF - 'decision_sem' : {'code':, 'code_prev': }, - 'decisions_ue' : { ue_id : { 'code' : ADM|CMP|AJ, 'event_date' :, - 'acronyme', 'numero': } }, - 'autorisations' : [ { 'semestre_id' : { ... } } ], - 'validation_parcours' : True si parcours validé (diplome obtenu) - 'prev_code' : code (calculé slt si with_prev), - 'mention' : mention (en fct moy gen), - 'sum_ects' : total ECTS acquis dans ce semestre (incluant les UE capitalisées) - 'sum_ects_capitalises' : somme des ECTS des UE capitalisees - } - ] - }, - 'decisions_dict' : { etudid : decision (comme ci-dessus) }, - } - """ - formsemestre = FormSemestre.query.get_or_404(formsemestre_id) - nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) - if etudids is None: - etudids = nt.get_etudids() - if not etudids: - return {} - cnx = ndb.GetDBConnexion() - sem = sco_formsemestre.get_formsemestre(formsemestre_id) - max_date = "0000-01-01" - has_prev = False # vrai si au moins un etudiant a un code prev - semestre_non_terminal = False # True si au moins un etudiant a un devenir - - decisions = [] - D = {} # même chose que decisions, mais { etudid : dec } - for etudid in etudids: - # etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] - etud: Identite = Identite.query.get(etudid) - Se = sco_cursus.get_situation_etud_cursus( - etud.to_dict_scodoc7(), formsemestre_id - ) - semestre_non_terminal = semestre_non_terminal or Se.semestre_non_terminal - d = {} - d["identite"] = nt.identdict[etudid] - d["etat"] = nt.get_etud_etat( - etudid - ) # I|D|DEF (inscription ou démission ou défaillant) - d["decision_sem"] = nt.get_etud_decision_sem(etudid) - d["decisions_ue"] = nt.get_etud_decision_ues(etudid) - if formsemestre.formation.is_apc(): - d.update(but_validations.dict_decision_jury(etud, formsemestre)) - d["last_formsemestre_id"] = Se.get_semestres()[ - -1 - ] # id du dernier semestre (chronologiquement) dans lequel il a été inscrit - - ects_capitalises_by_ue_code = _comp_ects_capitalises_by_ue_code(nt, etudid) - d["sum_ects_capitalises"] = sum(ects_capitalises_by_ue_code.values()) - ects_by_ue_code = _comp_ects_by_ue_code(nt, d["decisions_ue"]) - d["sum_ects"] = _sum_ects_dicts(ects_capitalises_by_ue_code, ects_by_ue_code) - - if d["decision_sem"] and codes_cursus.code_semestre_validant( - d["decision_sem"]["code"] - ): - d["mention"] = scu.get_mention(nt.get_etud_moy_gen(etudid)) - else: - d["mention"] = "" - # Versions "en français": (avec les UE capitalisées d'ailleurs) - dec_ue_list = _descr_decisions_ues( - nt, etudid, d["decisions_ue"], d["decision_sem"] - ) - d["decisions_ue_nb"] = len( - dec_ue_list - ) # avec les UE capitalisées, donc des éventuels doublons - # Mais sur la description (eg sur les bulletins), on ne veut pas - # afficher ces doublons: on uniquifie sur ue_code - _codes = set() - ue_uniq = [] - for ue in dec_ue_list: - if ue["ue_code"] not in _codes: - ue_uniq.append(ue) - _codes.add(ue["ue_code"]) - - d["decisions_ue_descr"] = ", ".join([ue["acronyme"] for ue in ue_uniq]) - if nt.is_apc: - d["decision_sem_descr"] = "" # pas de validation de semestre en BUT - else: - d["decision_sem_descr"] = _descr_decision_sem(d["etat"], d["decision_sem"]) - - autorisations = ScolarAutorisationInscription.query.filter_by( - etudid=etudid, origin_formsemestre_id=formsemestre_id - ).all() - d["autorisations"] = [a.to_dict() for a in autorisations] - d["autorisations_descr"] = descr_autorisations(autorisations) - - d["validation_parcours"] = Se.parcours_validated() - d["parcours"] = Se.get_cursus_descr(filter_futur=True) - if with_parcours_decisions: - d["parcours_decisions"] = Se.get_parcours_decisions() - # Observations sur les compensations: - compensators = sco_cursus_dut.scolar_formsemestre_validation_list( - cnx, args={"compense_formsemestre_id": formsemestre_id, "etudid": etudid} - ) - obs = [] - for compensator in compensators: - # nb: il ne devrait y en avoir qu'un ! - csem = sco_formsemestre.get_formsemestre(compensator["formsemestre_id"]) - obs.append( - "%s compensé par %s (%s)" - % (sem["sem_id_txt"], csem["sem_id_txt"], csem["anneescolaire"]) - ) - - if d["decision_sem"] and d["decision_sem"]["compense_formsemestre_id"]: - compensed = sco_formsemestre.get_formsemestre( - d["decision_sem"]["compense_formsemestre_id"] - ) - obs.append( - f"""{sem["sem_id_txt"]} compense {compensed["sem_id_txt"]} ({compensed["anneescolaire"]})""" - ) - - d["observation"] = ", ".join(obs) - - # Cherche la date de decision (sem ou UE) la plus récente: - if d["decision_sem"]: - date = ndb.DateDMYtoISO(d["decision_sem"]["event_date"]) - if date and date > max_date: # decision plus recente - max_date = date - if d["decisions_ue"]: - for dec_ue in d["decisions_ue"].values(): - if dec_ue: - date = ndb.DateDMYtoISO(dec_ue["event_date"]) - if date and date > max_date: # decision plus recente - max_date = date - # Code semestre precedent - if with_prev: # optionnel car un peu long... - info = sco_etud.get_etud_info(etudid=etudid, filled=True) - if not info: - continue # should not occur - etud = info[0] - if Se.prev and Se.prev_decision: - d["prev_decision_sem"] = Se.prev_decision - d["prev_code"] = Se.prev_decision["code"] - d["prev_code_descr"] = _descr_decision_sem( - scu.INSCRIT, Se.prev_decision - ) - d["prev"] = Se.prev - has_prev = True - else: - d["prev_decision_sem"] = None - d["prev_code"] = "" - d["prev_code_descr"] = "" - d["Se"] = Se - - decisions.append(d) - D[etudid] = d - - return { - "date": ndb.DateISOtoDMY(max_date), - "formsemestre": sem, - "is_apc": nt.is_apc, - "has_prev": has_prev, - "semestre_non_terminal": semestre_non_terminal, - "formation": sco_formations.formation_list( - args={"formation_id": sem["formation_id"]} - )[0], - "decisions": decisions, - "decisions_dict": D, - } - - def pvjury_table( dpv, only_diplome=False, @@ -501,7 +227,7 @@ def formsemestre_pvjury(formsemestre_id, format="html", publish=True): footer = html_sco_header.sco_footer() - dpv = sco_pv_dict.dict_pvjury(formsemestre_id, with_prev=True) + dpv = sco_dict_pv_jury.dict_pvjury(formsemestre_id, with_prev=True) if not dpv: if format == "html": return ( @@ -683,7 +409,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_pv_pdf.pvjury_pdf( + pdfdoc = sco_pvpdf.pvjury_pdf( formsemestre, etudids, numero_arrete=tf[2]["numero_arrete"], diff --git a/app/scodoc/sco_pvpdf.py b/app/scodoc/sco_pvpdf.py index ed9cd775..765d850f 100644 --- a/app/scodoc/sco_pvpdf.py +++ b/app/scodoc/sco_pvpdf.py @@ -49,6 +49,7 @@ 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 @@ -384,9 +385,7 @@ def pdf_lettres_individuelles( (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. """ - from app.scodoc import sco_pvjury - - dpv = sco_pvjury.dict_pvjury(formsemestre_id, etudids=etudids, with_prev=True) + dpv = sco_dict_pv_jury.dict_pvjury(formsemestre_id, etudids=etudids, with_prev=True) if not dpv: return "" # Ajoute infos sur etudiants @@ -656,59 +655,56 @@ def add_apc_infos(formsemestre: FormSemestre, params: dict, decision: dict): # ---------------------------------------------- def pvjury_pdf( - dpv, + formsemestre: FormSemestre, + etudids: list[int], date_commission=None, date_jury=None, - numeroArrete=None, - VDICode=None, - showTitle=False, + 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) - dpv: result of dict_pvjury """ - if not dpv: - return {} - sem = dpv["formsemestre"] - formsemestre_id = sem["formsemestre_id"] - - objects = _pvjury_pdf_type( - dpv, + objects, a_diplome = _pvjury_pdf_type( + formsemestre, + etudids, only_diplome=False, date_commission=date_commission, - numeroArrete=numeroArrete, - VDICode=VDICode, + numero_arrete=numero_arrete, + code_vdi=code_vdi, date_jury=date_jury, - showTitle=showTitle, + show_title=show_title, pv_title=pv_title, with_paragraph_nom=with_paragraph_nom, anonymous=anonymous, ) + if not objects: + return b"" - jury_de_diplome = not dpv["semestre_non_terminal"] + 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: - validations_parcours = [x["validation_parcours"] for x in dpv["decisions"]] - if True in validations_parcours: - # au moins un etudiant a validé son diplome: - objects.append(PageBreak()) - objects += _pvjury_pdf_type( - dpv, - only_diplome=True, - date_commission=date_commission, - date_jury=date_jury, - numeroArrete=numeroArrete, - VDICode=VDICode, - showTitle=showTitle, - pv_title=pv_title, - with_paragraph_nom=with_paragraph_nom, - anonymous=anonymous, - ) + 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 @@ -717,10 +713,10 @@ def pvjury_pdf( document.addPageTemplates( PVTemplate( document, - author="%s %s (E. Viennet)" % (sco_version.SCONAME, sco_version.SCOVERSION), - title=SU("PV du jury de %s" % sem["titre_num"]), + 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), + preferences=sco_preferences.SemPreferences(formsemestre.id), ) ) @@ -730,7 +726,8 @@ def pvjury_pdf( def _pvjury_pdf_type( - dpv, + formsemestre: FormSemestre, + etudids: list[int], only_diplome=False, date_commission=None, date_jury=None, @@ -740,20 +737,18 @@ def _pvjury_pdf_type( pv_title=None, anonymous=False, with_paragraph_nom=False, -): - """Doc PDF récapitulant les décisions de jury pour un type de jury (passage ou delivrance) - dpv: result of dict_pvjury +) -> 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 - # Jury de diplome si sem. terminal OU que l'on demande les diplomés d'un semestre antérieur - diplome = (not dpv["semestre_non_terminal"]) or only_diplome - - sem = dpv["formsemestre"] - formsemestre_id = sem["formsemestre_id"] - formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id) + 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 dpv["formation"]["titre_officiel"] + titre_diplome = pv_title or formsemestre.formation.titre_officiel objects = [] style = reportlab.lib.styles.ParagraphStyle({}) @@ -780,14 +775,11 @@ def _pvjury_pdf_type( objects += [Spacer(0, 5 * mm)] objects += sco_pdf.make_paras( - """ - Procès-verbal de %s du département %s - Session unique %s - """ - % ( - titre_jury, - sco_preferences.get_preference("DeptName", formsemestre_id) or "(sans nom)", - sem["anneescolaire"], - ), + 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, ) @@ -803,7 +795,7 @@ def _pvjury_pdf_type( objects += sco_pdf.make_paras( """Semestre: %s""" % sem["titre"], style ) - if sco_preferences.get_preference("PV_TITLE_WITH_VDI", formsemestre_id): + if sco_preferences.get_preference("PV_TITLE_WITH_VDI", formsemestre.id): objects += sco_pdf.make_paras( """VDI et Code: %s""" % (VDICode or ""), style ) @@ -815,11 +807,11 @@ def _pvjury_pdf_type( objects += sco_pdf.make_paras( "" - + (sco_preferences.get_preference("PV_INTRO", formsemestre_id) or "") + + (sco_preferences.get_preference("PV_INTRO", formsemestre.id) or "") % { - "Decnum": numeroArrete, - "VDICode": VDICode, - "UnivName": sco_preferences.get_preference("UnivName", formsemestre_id), + "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, @@ -832,12 +824,26 @@ def _pvjury_pdf_type( """Le jury propose les décisions suivantes :""", style ) objects += [Spacer(0, 4 * mm)] - lines, titles, columns_ids = sco_pvjury.pvjury_table( - dpv, - only_diplome=only_diplome, - anonymous=anonymous, - with_paragraph_nom=with_paragraph_nom, - ) + + 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] @@ -845,11 +851,11 @@ def _pvjury_pdf_type( # 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 + "SCOLAR_FONT_SIZE", formsemestre.id ) - cell_style.fontName = sco_preferences.get_preference("PV_FONTNAME", 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 + "SCOLAR_FONT_SIZE", formsemestre.id ) # vertical space LINEWIDTH = 0.5 table_style = [ @@ -857,7 +863,7 @@ def _pvjury_pdf_type( "FONTNAME", (0, 0), (-1, 0), - sco_preferences.get_preference("PV_FONTNAME", formsemestre_id), + 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)), @@ -872,22 +878,28 @@ def _pvjury_pdf_type( else: return x - Pt = [[_format_pv_cell(x) for x in line[1:]] for line in ([titles] + lines)] - widths = [6 * cm, 2.8 * cm, 2.8 * cm, None, None, None, None] - if dpv["has_prev"]: - widths[2:2] = [2.8 * cm] - if sco_preferences.get_preference("bul_show_mention", formsemestre_id): - widths += [None] - objects.append(Table(Pt, repeatRows=1, colWidths=widths, style=table_style)) + 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( - """ - %s, %s""" - % ( - sco_preferences.get_preference("DirectorName", formsemestre_id) or "", - sco_preferences.get_preference("DirectorTitle", formsemestre_id) or "", - ), + f"""{ + sco_preferences.get_preference("DirectorName", formsemestre.id) or "" + }, { + sco_preferences.get_preference("DirectorTitle", formsemestre.id) or "" + }""", style, ) @@ -907,7 +919,7 @@ def _pvjury_pdf_type( "FONTNAME", (0, 0), (-1, 0), - sco_preferences.get_preference("PV_FONTNAME", formsemestre_id), + 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)), @@ -922,4 +934,4 @@ def _pvjury_pdf_type( ) ) - return objects + return objects, a_diplome