From 998820e671b19f03e80a1c3a4480d983914a8d88 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 19 Feb 2023 15:45:27 +0100 Subject: [PATCH] =?UTF-8?q?R=C3=A9organisation=20du=20code=20de=20g=C3=A9n?= =?UTF-8?q?ration=20de=20PV=20de=20jury=20PDF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/jury_but_pv.py | 2 +- app/but/jury_but_results.py | 4 +- app/scodoc/sco_archives.py | 72 +- app/scodoc/sco_bulletins.py | 4 +- app/scodoc/sco_export_results.py | 6 +- app/scodoc/sco_formsemestre_validation.py | 4 +- app/scodoc/sco_inscr_passage.py | 4 +- app/scodoc/sco_pdf.py | 7 +- .../{sco_dict_pv_jury.py => sco_pv_dict.py} | 0 app/scodoc/{sco_pvjury.py => sco_pv_forms.py} | 37 +- app/scodoc/sco_pv_lettres_inviduelles.py | 357 +++++++ app/scodoc/sco_pv_pdf.py | 340 +++++++ app/scodoc/sco_pv_templates.py | 344 +++++++ app/scodoc/sco_pvpdf.py | 942 ------------------ app/scodoc/sco_report.py | 1 - app/templates/about.j2 | 13 + app/views/notes.py | 11 +- sco_version.py | 11 +- tests/unit/yaml_setup_but.py | 4 +- 19 files changed, 1133 insertions(+), 1030 deletions(-) rename app/scodoc/{sco_dict_pv_jury.py => sco_pv_dict.py} (100%) rename app/scodoc/{sco_pvjury.py => sco_pv_forms.py} (95%) create mode 100644 app/scodoc/sco_pv_lettres_inviduelles.py create mode 100644 app/scodoc/sco_pv_pdf.py create mode 100644 app/scodoc/sco_pv_templates.py delete mode 100644 app/scodoc/sco_pvpdf.py diff --git a/app/but/jury_but_pv.py b/app/but/jury_but_pv.py index b3ffba7799..32dd5a0f7f 100644 --- a/app/but/jury_but_pv.py +++ b/app/but/jury_but_pv.py @@ -97,7 +97,7 @@ def pvjury_table_but( """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": "Nom", diff --git a/app/but/jury_but_results.py b/app/but/jury_but_results.py index d947bf8456..f6a208f3e6 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/scodoc/sco_archives.py b/app/scodoc/sco_archives.py index 3e3ff43aa3..839780fe30 100644 --- a/app/scodoc/sco_archives.py +++ b/app/scodoc/sco_archives.py @@ -43,8 +43,8 @@ Les maquettes Apogée pour l'export des notes sont dans /apo_csv//-//.csv - Un répertoire d'archive contient des fichiers quelconques, et un fichier texte nommé _description.txt - qui est une description (humaine, format libre) de l'archive. + Un répertoire d'archive contient des fichiers quelconques, et un fichier texte + nommé _description.txt qui est une description (humaine, format libre) de l'archive. """ from typing import Union @@ -61,7 +61,6 @@ import chardet import flask from flask import flash, g, request, url_for -from flask_login import current_user import app.scodoc.sco_utils as scu from config import Config @@ -74,12 +73,11 @@ from app.scodoc.TrivialFormulator import TrivialFormulator 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_formsemestre 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 @@ -210,7 +208,7 @@ class BaseArchiver(object): self.initialize() filename = os.path.join(archive_id, "_description.txt") try: - with open(filename) as f: + with open(filename, encoding=scu.SCO_ENCODING) as f: descr = f.read() except UnicodeDecodeError: # some (old) files may have saved under exotic encodings @@ -294,7 +292,7 @@ PVArchive = SemsArchiver() def do_formsemestre_archive( formsemestre_id, - group_ids=[], # si indiqué, ne prend que ces groupes + group_ids: list[int] = None, # si indiqué, ne prend que ces groupes description="", date_jury="", signature=None, # pour lettres indiv @@ -349,7 +347,8 @@ def do_formsemestre_archive( no_side_bar=True, ), f'

Valeurs archivées le {date}

', - '', + """""", table_html, html_sco_header.sco_footer(), ] @@ -366,7 +365,7 @@ def do_formsemestre_archive( response = jury_but_pv.pvjury_page_but(formsemestre_id, fmt="xls") data = response.get_data() else: # formations classiques - data = sco_pvjury.formsemestre_pvjury( + data = sco_pv_forms.formsemestre_pvjury( formsemestre_id, format="xls", publish=False ) if data: @@ -382,7 +381,7 @@ def do_formsemestre_archive( if data: PVArchive.store(archive_id, "Bulletins.pdf", data) # Lettres individuelles (PDF): - data = sco_pvpdf.pdf_lettres_individuelles( + data = sco_pv_lettres_inviduelles.pdf_lettres_individuelles( formsemestre_id, etudids=etudids, date_jury=date_jury, @@ -390,27 +389,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): @@ -450,7 +445,11 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement. """, ] F = [ - """

Note: les documents sont aussi affectés par les réglages sur la page "Paramétrage" (accessible à l'administrateur du département). + f"""

Note: les documents sont aussi affectés par les réglages sur la page + "Paramétrage" + (accessible à l'administrateur du département).

""", html_sco_header.sco_footer(), ] @@ -462,7 +461,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", @@ -507,7 +506,7 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement. if tf[0] == 0: return "\n".join(H) + "\n" + tf[1] + "\n".join(F) elif tf[0] == -1: - msg = "Opération%20annulée" + msg = "Opération annulée" else: # submit sf = tf[2]["signature"] @@ -531,7 +530,7 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement. anonymous=tf[2]["anonymous"], bul_version=tf[2]["bul_version"], ) - msg = "Nouvelle%20archive%20créée" + msg = "Nouvelle archive créée" # submitted or cancelled: flash(msg) @@ -546,7 +545,6 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement. def formsemestre_list_archives(formsemestre_id): """Page listing archives""" - sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem_archive_id = formsemestre_id L = [] for archive_id in PVArchive.list_obj_archives(sem_archive_id): diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index 84e810a75d..96c85d69aa 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_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 @@ -787,7 +787,7 @@ def etud_descr_situation_semestre( infos["date_defaillance"] = date_def infos["descr_decision_jury"] = f"Défaillant{ne}" - dpv = sco_dict_pv_jury.dict_pvjury(formsemestre_id, etudids=[etudid]) + dpv = sco_pv_dict.dict_pvjury(formsemestre_id, etudids=[etudid]) if dpv: infos["decision_sem"] = dpv["decisions"][0]["decision_sem"] diff --git a/app/scodoc/sco_export_results.py b/app/scodoc/sco_export_results.py index 4d27ec1aab..d4e9a37840 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,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_dict_pv_jury.dict_pvjury( + dpv_by_sem[formsemestre_id] = sco_pv_dict.dict_pvjury( formsemestre_id, with_parcours_decisions=True ) @@ -348,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_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py index 49ca5de083..c5806fd2e9 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( @@ -562,7 +562,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_inscr_passage.py b/app/scodoc/sco_inscr_passage.py index 197b5169ed..1a5953be29 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 = [ diff --git a/app/scodoc/sco_pdf.py b/app/scodoc/sco_pdf.py index 7fca1e6790..ffa11d2594 100755 --- a/app/scodoc/sco_pdf.py +++ b/app/scodoc/sco_pdf.py @@ -221,7 +221,7 @@ class ScoDocPageTemplate(PageTemplate): def __init__( self, document, - pagesbookmarks={}, + pagesbookmarks: dict = None, author=None, title=None, subject=None, @@ -385,6 +385,11 @@ class BulletinDocTemplate(BaseDocTemplate): ajoute la gestion des bookmarks """ + def __init__(self, *args, **kw): + super().__init__(*args, **kw) + self.current_footer = "" + self.filigranne = None + # inspired by https://www.reportlab.com/snippets/13/ def afterFlowable(self, flowable): """Called by Reportlab after each flowable""" diff --git a/app/scodoc/sco_dict_pv_jury.py b/app/scodoc/sco_pv_dict.py similarity index 100% rename from app/scodoc/sco_dict_pv_jury.py rename to app/scodoc/sco_pv_dict.py diff --git a/app/scodoc/sco_pvjury.py b/app/scodoc/sco_pv_forms.py similarity index 95% rename from app/scodoc/sco_pvjury.py rename to app/scodoc/sco_pv_forms.py index 6ec676a4aa..c5b256e4de 100644 --- a/app/scodoc/sco_pvjury.py +++ b/app/scodoc/sco_pv_forms.py @@ -27,23 +27,7 @@ """Edition des PV de jury -PV Jury IUTV 2006: on détaillait 8 cas: -Jury de semestre n - On a 8 types de décisions: - Passages: - 1. passage de ceux qui ont validés Sn-1 - 2. passage avec compensation Sn-1, Sn - 3. passage sans validation de Sn avec validation d'UE - 4. passage sans validation de Sn sans validation d'UE - - Redoublements: - 5. redoublement de Sn-1 et Sn sans validation d'UE pour Sn - 6. redoublement de Sn-1 et Sn avec validation d'UE pour Sn - - Reports - 7. report sans validation d'UE - - 8. non validation de Sn-1 et Sn et non redoublement +Formulaires paramétrage PV et génération des tables """ import time @@ -54,25 +38,20 @@ 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_formsemestre from app.scodoc import sco_groups from app.scodoc import sco_groups_view from app.scodoc import sco_pdf from app.scodoc import sco_preferences -from app.scodoc import sco_pvpdf +from app.scodoc import sco_pv_pdf +from app.scodoc import sco_pv_lettres_inviduelles from app.scodoc.gen_tables import GenTable from app.scodoc.codes_cursus import NO_SEMESTRE_ID from app.scodoc.sco_pdf import PDFLOCK @@ -245,7 +224,7 @@ def formsemestre_pvjury(formsemestre_id, format="html", publish=True): # XXX 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 ( @@ -427,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"], @@ -596,7 +575,7 @@ def formsemestre_lettres_individuelles(formsemestre_id, group_ids=[]): signature = sf.read() # image of signature try: PDFLOCK.acquire() - pdfdoc = sco_pvpdf.pdf_lettres_individuelles( + pdfdoc = sco_pv_lettres_inviduelles.pdf_lettres_individuelles( formsemestre_id, etudids=etudids, date_jury=tf[2]["date_jury"], diff --git a/app/scodoc/sco_pv_lettres_inviduelles.py b/app/scodoc/sco_pv_lettres_inviduelles.py new file mode 100644 index 0000000000..42f5d41368 --- /dev/null +++ b/app/scodoc/sco_pv_lettres_inviduelles.py @@ -0,0 +1,357 @@ +# -*- 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 lettres individuelles de jury +""" +# code initialement dans sco_pvpdf.py + +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 +from reportlab.platypus import PageBreak, Table, Image +from reportlab.platypus.doctemplate import BaseDocTemplate +from reportlab.lib import styles + +from app.models import FormSemestre, Identite + +import app.scodoc.sco_utils as scu +from app.scodoc import sco_bulletins_pdf +from app.scodoc import sco_pv_dict +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_cursus_dut import SituationEtudCursus +from app.scodoc.sco_pv_templates import CourrierIndividuelTemplate, jury_titres +import sco_version + + +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_pv_dict.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 _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_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 = jury_titres( + 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 ""} + """ diff --git a/app/scodoc/sco_pv_pdf.py b/app/scodoc/sco_pv_pdf.py new file mode 100644 index 0000000000..c9473ba1e6 --- /dev/null +++ b/app/scodoc/sco_pv_pdf.py @@ -0,0 +1,340 @@ +# -*- 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 +# +############################################################################## + +"""Génération du PV de jury en PDF (celui en format paysage avec l'ensemble des décisions) +""" +import io + +import reportlab +from reportlab.lib.units import cm, mm +from reportlab.lib.enums import TA_JUSTIFY +from reportlab.platypus import ( + Paragraph, + Spacer, + PageBreak, + Table, +) +from reportlab.platypus.doctemplate import BaseDocTemplate +from reportlab.lib.pagesizes import A4, landscape +from reportlab.lib import styles +from reportlab.lib.colors import Color + +from app.models import FormSemestre + +from app.scodoc import codes_cursus +from app.scodoc import sco_pv_dict +from app.scodoc import sco_pdf +from app.scodoc import sco_preferences +from app.scodoc.sco_pdf import SU +from app.scodoc.sco_pv_templates import PVTemplate, jury_titres +import sco_version + +# ---------------------------------------------- +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 _make_pv_styles(formsemestre: FormSemestre): + 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 + style_bullet = reportlab.lib.styles.ParagraphStyle({}) + style_bullet.fontSize = 12 + style_bullet.fontName = sco_preferences.get_preference( + "PV_FONTNAME", formsemestre.id + ) + style_bullet.leading = 12 + style_bullet.alignment = TA_JUSTIFY + style_bullet.firstLineIndent = 0 + style_bullet.leftIndent = indent + style_bullet.bulletIndent = indent + style_bullet.bulletFontName = "Times-Roman" + style_bullet.bulletFontSize = 11 + style_bullet.spaceBefore = 5 * mm + style_bullet.spaceAfter = 5 * mm + return style, style_bullet + + +def _pvjury_pdf_type( + formsemestre: FormSemestre, + etudids: list[int], + only_diplome=False, + date_commission=None, + date_jury=None, + numero_arrete=None, + code_vdi=None, + show_title=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_pv_forms + from app.but import jury_but_pv + + 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, _ = jury_titres(formsemestre, diplome) + titre_diplome = pv_title or formsemestre.formation.titre_officiel + objects = [] + + style, style_bullet = _make_pv_styles(formsemestre) + + 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( + f"""{titre_diplome}""", + style, + ) + + if show_title: + objects += sco_pdf.make_paras( + f"""Semestre: {formsemestre.titre}""", + style, + ) + if sco_preferences.get_preference("PV_TITLE_WITH_VDI", formsemestre.id): + objects += sco_pdf.make_paras( + f"""VDI et Code: {(code_vdi or "")}""", style + ) + + if date_jury: + objects += sco_pdf.make_paras( + f"""Jury tenu le {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, + } + + "", + style_bullet, + ) + + 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_pv_dict.dict_pvjury(formsemestre.id, etudids=etudids, with_prev=True) + if not dpv: + return [], False + rows, titles, columns_ids = sco_pv_forms.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 + rows = [[line.get(x, "") for x in columns_ids] for line in rows] + 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 = [f"{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_pv_templates.py b/app/scodoc/sco_pv_templates.py new file mode 100644 index 0000000000..da1c963a7d --- /dev/null +++ b/app/scodoc/sco_pv_templates.py @@ -0,0 +1,344 @@ +# -*- 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 + +import app.scodoc.sco_utils as scu +from app.scodoc import sco_pdf +from app.scodoc.sco_logos import find_logo +from app.scodoc.sco_pdf import SU + +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) + col_widths = [None, LOGO_FOOTER_WIDTH + 2 * mm] + if with_page_numbers: + elems.append(np) + col_widths.append(2 * cm) + else: + elems.append("") + col_widths.append(8 * mm) # force marge droite + tab = Table([elems], style=tabstyle, colWidths=col_widths) + 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 is not 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"], + ) + super().__init__( + 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 + # super().beforeDrawPage(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 jury_titres(formsemestre: FormSemestre, diplome: bool) -> tuple[str, str]: + """Titres du PV ou lettre de jury""" + 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 diff --git a/app/scodoc/sco_pvpdf.py b/app/scodoc/sco_pvpdf.py deleted file mode 100644 index deeea3933f..0000000000 --- a/app/scodoc/sco_pvpdf.py +++ /dev/null @@ -1,942 +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 _make_pv_styles(formsemestre: FormSemestre): - 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 - style_bullet = reportlab.lib.styles.ParagraphStyle({}) - style_bullet.fontSize = 12 - style_bullet.fontName = sco_preferences.get_preference( - "PV_FONTNAME", formsemestre.id - ) - style_bullet.leading = 12 - style_bullet.alignment = TA_JUSTIFY - style_bullet.firstLineIndent = 0 - style_bullet.leftIndent = indent - style_bullet.bulletIndent = indent - style_bullet.bulletFontName = "Times-Roman" - style_bullet.bulletFontSize = 11 - style_bullet.spaceBefore = 5 * mm - style_bullet.spaceAfter = 5 * mm - return style, style_bullet - - -def _pvjury_pdf_type( - formsemestre: FormSemestre, - etudids: list[int], - only_diplome=False, - date_commission=None, - date_jury=None, - numero_arrete=None, - code_vdi=None, - show_title=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 - from app.but import jury_but_pv - - 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, style_bullet = _make_pv_styles(formsemestre) - - 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( - f"""{titre_diplome}""", - style, - ) - - if show_title: - objects += sco_pdf.make_paras( - f"""Semestre: {formsemestre.titre}""", - style, - ) - if sco_preferences.get_preference("PV_TITLE_WITH_VDI", formsemestre.id): - objects += sco_pdf.make_paras( - f"""VDI et Code: {(code_vdi or "")}""", style - ) - - if date_jury: - objects += sco_pdf.make_paras( - f"""Jury tenu le {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, - } - + "", - style_bullet, - ) - - 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 - rows = [[line.get(x, "") for x in columns_ids] for line in rows] - 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 = [f"{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_report.py b/app/scodoc/sco_report.py index 4ba47dc815..7e2e7cfde7 100644 --- a/app/scodoc/sco_report.py +++ b/app/scodoc/sco_report.py @@ -54,7 +54,6 @@ from app.scodoc import sco_etud from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_preferences -from app.scodoc import sco_pvjury import sco_version from app.scodoc.gen_tables import GenTable from app import log diff --git a/app/templates/about.j2 b/app/templates/about.j2 index 3dff1edee5..fa322e44e6 100644 --- a/app/templates/about.j2 +++ b/app/templates/about.j2 @@ -15,6 +15,19 @@ Information et documentation sur scodoc.org.

+

Le logiciel est distribué sous + licence GNU + GPL v2. ScoDoc est un logiciel réalisé dans l'espoir d'être utile + mais distribué "en l'état" sans aucune garantie de quelque nature que ce + soit, expresse ou ou implicite, y compris, mais sans y être limité, les + garanties implicites de commerciabilité et de la conformité a une + utilisation particulière. Vous assumez la totalité des risques liés à la + qualité et aux performances du programme. Si le programme se révélait + défectueux, le coût de l'entretien, des réparations ou des corrections + nécessaires vous incombent intégralement. + +

+

Dernières évolutions

{{ news|safe }} diff --git a/app/views/notes.py b/app/views/notes.py index 85a53dc603..429adf387b 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -122,12 +122,11 @@ from app.scodoc import sco_lycee from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl_inscriptions from app.scodoc import sco_moduleimpl_status -from app.scodoc import sco_permissions_check from app.scodoc import sco_placement from app.scodoc import sco_poursuite_dut from app.scodoc import sco_preferences from app.scodoc import sco_prepajury -from app.scodoc import sco_pvjury +from app.scodoc import sco_pv_forms from app.scodoc import sco_recapcomplet from app.scodoc import sco_report from app.scodoc import sco_report_but @@ -2803,7 +2802,9 @@ def formsemestre_validation_suppress_etud( # ------------- PV de JURY et archives -sco_publish("/formsemestre_pvjury", sco_pvjury.formsemestre_pvjury, Permission.ScoView) +sco_publish( + "/formsemestre_pvjury", sco_pv_forms.formsemestre_pvjury, Permission.ScoView +) sco_publish("/pvjury_page_but", jury_but_pv.pvjury_page_but, Permission.ScoView) @@ -2913,12 +2914,12 @@ def formsemestre_jury_but_erase( sco_publish( "/formsemestre_lettres_individuelles", - sco_pvjury.formsemestre_lettres_individuelles, + sco_pv_forms.formsemestre_lettres_individuelles, Permission.ScoView, methods=["GET", "POST"], ) sco_publish( - "/formsemestre_pvjury_pdf", sco_pvjury.formsemestre_pvjury_pdf, Permission.ScoView + "/formsemestre_pvjury_pdf", sco_pv_forms.formsemestre_pvjury_pdf, Permission.ScoView ) sco_publish( "/feuille_preparation_jury", diff --git a/sco_version.py b/sco_version.py index 0442f9c277..98786ca066 100644 --- a/sco_version.py +++ b/sco_version.py @@ -6,6 +6,15 @@ SCOVERSION = "9.4.45" SCONAME = "ScoDoc" SCONEWS = """ +

Année 2023

+
    +
  • ScoDoc 9.4
  • +
      +
    • Améliorations des tableaux récapitulatifs
    • +
    • Nouvelle interface de gestions des groupes (S. Lehmann)
    • +
    • Enrichissement des jurys BUT et des procès-verbaux associés.
    • +
    +

Année 2022

  • ScoDoc 9.4
  • @@ -14,7 +23,7 @@ SCONEWS = """
  • ScoDoc 9.3
    • -
    • Nouvelle API REST pour connecter ScoDoc à d'autres applications
    • +
    • Nouvelle API REST pour connecter ScoDoc à d'autres applications
    • Module de gestion des relations avec les entreprises
    • Prise en charge des parcours BUT
    • Association des UEs aux compétences du référentiel
    • diff --git a/tests/unit/yaml_setup_but.py b/tests/unit/yaml_setup_but.py index 516fd73586..864a2667e9 100644 --- a/tests/unit/yaml_setup_but.py +++ b/tests/unit/yaml_setup_but.py @@ -29,7 +29,7 @@ from app.models import ( UniteEns, ) from app.scodoc import sco_utils as scu -from app.scodoc import sco_dict_pv_jury +from app.scodoc import sco_pv_dict def setup_formation_referentiel(formation: Formation, refcomp_infos: dict): @@ -308,7 +308,7 @@ def but_test_jury(formsemestre: FormSemestre, doc: dict): but_compare_decisions_annee(deca, deca_att) if "autorisations_inscription" in doc_formsemestre["attendu"]: if dpv is None: # lazy load - dpv = sco_dict_pv_jury.dict_pvjury(formsemestre.id) + dpv = sco_pv_dict.dict_pvjury(formsemestre.id) check_autorisations_inscription( etud, dpv, doc_formsemestre["attendu"]["autorisations_inscription"] )