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"]
)