Réorganisation du code de génration de PV de jury PDF

This commit is contained in:
Emmanuel Viennet 2023-02-19 15:45:27 +01:00
parent 52dfa16a69
commit 998820e671
19 changed files with 1133 additions and 1030 deletions

View File

@ -97,7 +97,7 @@ def pvjury_table_but(
"""Table avec résultats jury BUT pour PV. """Table avec résultats jury BUT pour PV.
Si etudids est None, prend tous les étudiants inscrits. 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 annee_but = (formsemestre.semestre_id + 1) // 2
titles = { titles = {
"nom": "Nom", "nom": "Nom",

View File

@ -12,7 +12,7 @@ import numpy as np
from app.but import jury_but from app.but import jury_but
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.formsemestre import FormSemestre 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]: 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: if formsemestre.formation.referentiel_competence is None:
# pas de ref. comp., donc pas de decisions de jury (ne lance pas d'exception) # pas de ref. comp., donc pas de decisions de jury (ne lance pas d'exception)
return [] return []
dpv = sco_dict_pv_jury.dict_pvjury(formsemestre.id) dpv = sco_pv_dict.dict_pvjury(formsemestre.id)
rows = [] rows = []
for etudid in formsemestre.etuds_inscriptions: for etudid in formsemestre.etuds_inscriptions:
rows.append(_get_jury_but_etud_result(formsemestre, dpv, etudid)) rows.append(_get_jury_but_etud_result(formsemestre, dpv, etudid))

View File

@ -43,8 +43,8 @@
Les maquettes Apogée pour l'export des notes sont dans Les maquettes Apogée pour l'export des notes sont dans
<archivedir>/apo_csv/<dept_id>/<annee_scolaire>-<sem_id>/<YYYY-MM-DD-HH-MM-SS>/<code_etape>.csv <archivedir>/apo_csv/<dept_id>/<annee_scolaire>-<sem_id>/<YYYY-MM-DD-HH-MM-SS>/<code_etape>.csv
Un répertoire d'archive contient des fichiers quelconques, et un fichier texte nommé _description.txt Un répertoire d'archive contient des fichiers quelconques, et un fichier texte
qui est une description (humaine, format libre) de l'archive. nommé _description.txt qui est une description (humaine, format libre) de l'archive.
""" """
from typing import Union from typing import Union
@ -61,7 +61,6 @@ import chardet
import flask import flask
from flask import flash, g, request, url_for from flask import flash, g, request, url_for
from flask_login import current_user
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from config import Config from config import Config
@ -74,12 +73,11 @@ from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.sco_exceptions import ScoPermissionDenied from app.scodoc.sco_exceptions import ScoPermissionDenied
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_bulletins_pdf 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
from app.scodoc import sco_groups_view from app.scodoc import sco_groups_view
from app.scodoc import sco_pvjury from app.scodoc import sco_pv_forms
from app.scodoc import sco_dict_pv_jury from app.scodoc import sco_pv_lettres_inviduelles
from app.scodoc import sco_pvpdf from app.scodoc import sco_pv_pdf
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
@ -210,7 +208,7 @@ class BaseArchiver(object):
self.initialize() self.initialize()
filename = os.path.join(archive_id, "_description.txt") filename = os.path.join(archive_id, "_description.txt")
try: try:
with open(filename) as f: with open(filename, encoding=scu.SCO_ENCODING) as f:
descr = f.read() descr = f.read()
except UnicodeDecodeError: except UnicodeDecodeError:
# some (old) files may have saved under exotic encodings # some (old) files may have saved under exotic encodings
@ -294,7 +292,7 @@ PVArchive = SemsArchiver()
def do_formsemestre_archive( def do_formsemestre_archive(
formsemestre_id, formsemestre_id,
group_ids=[], # si indiqué, ne prend que ces groupes group_ids: list[int] = None, # si indiqué, ne prend que ces groupes
description="", description="",
date_jury="", date_jury="",
signature=None, # pour lettres indiv signature=None, # pour lettres indiv
@ -349,7 +347,8 @@ def do_formsemestre_archive(
no_side_bar=True, no_side_bar=True,
), ),
f'<h2 class="fontorange">Valeurs archivées le {date}</h2>', f'<h2 class="fontorange">Valeurs archivées le {date}</h2>',
'<style type="text/css">table.notes_recapcomplet tr { color: rgb(185,70,0); }</style>', """<style type="text/css">table.notes_recapcomplet tr { color: rgb(185,70,0); }
</style>""",
table_html, table_html,
html_sco_header.sco_footer(), html_sco_header.sco_footer(),
] ]
@ -366,7 +365,7 @@ def do_formsemestre_archive(
response = jury_but_pv.pvjury_page_but(formsemestre_id, fmt="xls") response = jury_but_pv.pvjury_page_but(formsemestre_id, fmt="xls")
data = response.get_data() data = response.get_data()
else: # formations classiques else: # formations classiques
data = sco_pvjury.formsemestre_pvjury( data = sco_pv_forms.formsemestre_pvjury(
formsemestre_id, format="xls", publish=False formsemestre_id, format="xls", publish=False
) )
if data: if data:
@ -382,7 +381,7 @@ def do_formsemestre_archive(
if data: if data:
PVArchive.store(archive_id, "Bulletins.pdf", data) PVArchive.store(archive_id, "Bulletins.pdf", data)
# Lettres individuelles (PDF): # Lettres individuelles (PDF):
data = sco_pvpdf.pdf_lettres_individuelles( data = sco_pv_lettres_inviduelles.pdf_lettres_individuelles(
formsemestre_id, formsemestre_id,
etudids=etudids, etudids=etudids,
date_jury=date_jury, date_jury=date_jury,
@ -390,16 +389,12 @@ def do_formsemestre_archive(
signature=signature, signature=signature,
) )
if data: 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 # PV de jury (PDF):
# en BUT, le PV est sous forme excel (Decisions_Jury.xlsx ci-dessus) data = sco_pv_pdf.pvjury_pdf(
if not formsemestre.formation.is_apc(): formsemestre,
dpv = sco_dict_pv_jury.dict_pvjury( etudids=etudids,
formsemestre_id, etudids=etudids, with_prev=True
)
data = sco_pvpdf.pvjury_pdf(
dpv,
date_commission=date_commission, date_commission=date_commission,
date_jury=date_jury, date_jury=date_jury,
numero_arrete=numero_arrete, numero_arrete=numero_arrete,
@ -410,7 +405,7 @@ def do_formsemestre_archive(
anonymous=anonymous, anonymous=anonymous,
) )
if data: if data:
PVArchive.store(archive_id, "PV_Jury%s.pdf" % groups_filename, data) PVArchive.store(archive_id, f"PV_Jury{groups_filename}.pdf", data)
def formsemestre_archive(formsemestre_id, group_ids: list[int] = None): 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 = [ F = [
"""<p><em>Note: les documents sont aussi affectés par les réglages sur la page "<a href="edit_preferences">Paramétrage</a>" (accessible à l'administrateur du département).</em> f"""<p><em>Note: les documents sont aussi affectés par les réglages sur la page
"<a class="stdlink" href="{
url_for("scolar.edit_preferences", scodoc_dept=g.scodoc_dept)
}">Paramétrage</a>"
(accessible à l'administrateur du département).</em>
</p>""", </p>""",
html_sco_header.sco_footer(), 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"}), ("sep", {"input_type": "separator", "title": "Informations sur PV de jury"}),
] ]
descr += sco_pvjury.descrform_pvjury(formsemestre) descr += sco_pv_forms.descrform_pvjury(formsemestre)
descr += [ descr += [
( (
"signature", "signature",
@ -507,7 +506,7 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement.
if tf[0] == 0: if tf[0] == 0:
return "\n".join(H) + "\n" + tf[1] + "\n".join(F) return "\n".join(H) + "\n" + tf[1] + "\n".join(F)
elif tf[0] == -1: elif tf[0] == -1:
msg = "Opération%20annulée" msg = "Opération annulée"
else: else:
# submit # submit
sf = tf[2]["signature"] sf = tf[2]["signature"]
@ -531,7 +530,7 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement.
anonymous=tf[2]["anonymous"], anonymous=tf[2]["anonymous"],
bul_version=tf[2]["bul_version"], bul_version=tf[2]["bul_version"],
) )
msg = "Nouvelle%20archive%20créée" msg = "Nouvelle archive créée"
# submitted or cancelled: # submitted or cancelled:
flash(msg) flash(msg)
@ -546,7 +545,6 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement.
def formsemestre_list_archives(formsemestre_id): def formsemestre_list_archives(formsemestre_id):
"""Page listing archives""" """Page listing archives"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
sem_archive_id = formsemestre_id sem_archive_id = formsemestre_id
L = [] L = []
for archive_id in PVArchive.list_obj_archives(sem_archive_id): for archive_id in PVArchive.list_obj_archives(sem_archive_id):

View File

@ -59,7 +59,7 @@ from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_preferences 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 from app.scodoc import sco_users
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType, fmt_note from app.scodoc.sco_utils import ModuleType, fmt_note
@ -787,7 +787,7 @@ def etud_descr_situation_semestre(
infos["date_defaillance"] = date_def infos["date_defaillance"] = date_def
infos["descr_decision_jury"] = f"Défaillant{ne}" 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: if dpv:
infos["decision_sem"] = dpv["decisions"][0]["decision_sem"] infos["decision_sem"] = dpv["decisions"][0]["decision_sem"]

View File

@ -40,7 +40,7 @@ from app.scodoc import html_sco_header
from app.scodoc import sco_bac from app.scodoc import sco_bac
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
from app.scodoc import sco_preferences 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 from app.scodoc import sco_etud
import sco_version import sco_version
from app.scodoc.gen_tables import GenTable 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: # Décisions de jury de tous les semestres:
dpv_by_sem = {} dpv_by_sem = {}
for formsemestre_id in formsemestre_ids: 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 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) formsemestre_ids = get_set_formsemestre_id_dates( start_date, end_date)
dpv_by_sem = {} dpv_by_sem = {}
for formsemestre_id in formsemestre_ids: 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() ] semlist = [ dpv['formsemestre'] for dpv in dpv_by_sem.values() ]

View File

@ -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.sco_cursus_dut import etud_est_inscrit_ue
from app.scodoc import sco_photos from app.scodoc import sco_photos
from app.scodoc import sco_preferences 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( def formsemestre_validation_etud_form(
@ -562,7 +562,7 @@ def formsemestre_recap_parcours_table(
is_cur = Se.formsemestre_id == sem["formsemestre_id"] is_cur = Se.formsemestre_id == sem["formsemestre_id"]
num_sem += 1 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] pv = dpv["decisions"][0]
decision_sem = pv["decision_sem"] decision_sem = pv["decision_sem"]
decisions_ue = pv["decisions_ue"] decisions_ue = pv["decisions_ue"]

View File

@ -47,7 +47,7 @@ from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_preferences 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 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]: def list_etuds_from_sem(src, dst) -> list[dict]:
"""Liste des etudiants du semestre src qui sont autorisés à passer dans le semestre dst.""" """Liste des etudiants du semestre src qui sont autorisés à passer dans le semestre dst."""
target = dst["semestre_id"] 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: if not dpv:
return [] return []
etuds = [ etuds = [

View File

@ -221,7 +221,7 @@ class ScoDocPageTemplate(PageTemplate):
def __init__( def __init__(
self, self,
document, document,
pagesbookmarks={}, pagesbookmarks: dict = None,
author=None, author=None,
title=None, title=None,
subject=None, subject=None,
@ -385,6 +385,11 @@ class BulletinDocTemplate(BaseDocTemplate):
ajoute la gestion des bookmarks 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/ # inspired by https://www.reportlab.com/snippets/13/
def afterFlowable(self, flowable): def afterFlowable(self, flowable):
"""Called by Reportlab after each flowable""" """Called by Reportlab after each flowable"""

View File

@ -27,23 +27,7 @@
"""Edition des PV de jury """Edition des PV de jury
PV Jury IUTV 2006: on détaillait 8 cas: Formulaires paramétrage PV et génération des tables
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
""" """
import time import time
@ -54,25 +38,20 @@ import flask
from flask import flash, redirect, url_for from flask import flash, redirect, url_for
from flask import g, request from flask import g, request
from app.models import ( from app.models import FormSemestre, Identite
Formation,
FormSemestre,
ScolarAutorisationInscription,
)
from app.models.etudiants import Identite
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import codes_cursus 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_etud
from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_groups_view from app.scodoc import sco_groups_view
from app.scodoc import sco_pdf from app.scodoc import sco_pdf
from app.scodoc import sco_preferences 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.gen_tables import GenTable
from app.scodoc.codes_cursus import NO_SEMESTRE_ID from app.scodoc.codes_cursus import NO_SEMESTRE_ID
from app.scodoc.sco_pdf import PDFLOCK 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() 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 not dpv:
if format == "html": if format == "html":
return ( return (
@ -427,7 +406,7 @@ def formsemestre_pvjury_pdf(formsemestre_id, group_ids: list[int] = None, etudid
tf[2]["anonymous"] = bool(tf[2]["anonymous"]) tf[2]["anonymous"] = bool(tf[2]["anonymous"])
try: try:
PDFLOCK.acquire() PDFLOCK.acquire()
pdfdoc = sco_pvpdf.pvjury_pdf( pdfdoc = sco_pv_pdf.pvjury_pdf(
formsemestre, formsemestre,
etudids, etudids,
numero_arrete=tf[2]["numero_arrete"], numero_arrete=tf[2]["numero_arrete"],
@ -596,7 +575,7 @@ def formsemestre_lettres_individuelles(formsemestre_id, group_ids=[]):
signature = sf.read() # image of signature signature = sf.read() # image of signature
try: try:
PDFLOCK.acquire() PDFLOCK.acquire()
pdfdoc = sco_pvpdf.pdf_lettres_individuelles( pdfdoc = sco_pv_lettres_inviduelles.pdf_lettres_individuelles(
formsemestre_id, formsemestre_id,
etudids=etudids, etudids=etudids,
date_jury=tf[2]["date_jury"], date_jury=tf[2]["date_jury"],

View File

@ -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="<para>") -> str:
"""Reportlab bug turnaround (could be removed in a future version).
p is a string with Reportlab intra-paragraph XML tags.
Replaces <br> (currently ignored by Reportlab) by </para><para>
Also replaces <br> by <br/>
"""
return ("</para>" + para).join(
re.split(r"<.*?br.*?/>", paragraph_txt.replace("<br>", "<br/>"))
)
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", "<br/>")
# UE capitalisées:
if decision["decisions_ue"] and decision["decisions_ue_descr"]:
params["decision_ue_txt"] = (
"""<b>Unité%(decisions_ue_descr_plural)s d'Enseignement %(decision_orig)s capitalisée%(decisions_ue_descr_plural)s : %(decisions_ue_descr)s</b>"""
% params
)
else:
params["decision_ue_txt"] = ""
# Mention
params["mention"] = decision["mention"]
# Informations sur compensations
if decision["observation"]:
params["observation_txt"] = (
"""<b>Observation :</b> %(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 : <b>%s</b>""" % (
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 : <b>%(titre_formation)s</b>""" % 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, '<para leftindent="%(htab1)s">')
objects += sco_pdf.make_paras(
(
"""<para leftindent="%(htab1)s" spaceBefore="25mm">"""
+ sig
+ """</para>"""
)
% params,
style,
)
else:
sig = (
sco_preferences.get_preference(
"PV_LETTER_DIPLOMA_SIGNATURE", formsemestre_id
)
or ""
) % params
sig = _simulate_br(sig, '<para leftindent="%(htab1)s">')
objects += sco_pdf.make_paras(
(
"""<para leftindent="%(htab1)s" spaceBefore="25mm">"""
+ sig
+ """</para>"""
)
% 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"""<b>Décision du semestre antérieur S{params['prev_semestre_id']} :</b> {
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"]}<br/>
<b>Niveaux de compétences:</b><br/> {decision.get("descr_decisions_rcue") or ""}
"""

340
app/scodoc/sco_pv_pdf.py Normal file
View File

@ -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"""
<para align="center"><b>Procès-verbal de {titre_jury} du département {
sco_preferences.get_preference("DeptName", formsemestre.id) or "(sans nom)"
} - Session unique {formsemestre.annee_scolaire()}</b></para>
""",
style,
)
objects += sco_pdf.make_paras(
f"""<para align="center"><b><i>{titre_diplome}</i></b></para>""",
style,
)
if show_title:
objects += sco_pdf.make_paras(
f"""<para align="center"><b>Semestre: {formsemestre.titre}</b></para>""",
style,
)
if sco_preferences.get_preference("PV_TITLE_WITH_VDI", formsemestre.id):
objects += sco_pdf.make_paras(
f"""<para align="center">VDI et Code: {(code_vdi or "")}</para>""", style
)
if date_jury:
objects += sco_pdf.make_paras(
f"""<para align="center">Jury tenu le {date_jury}</para>""", style
)
objects += sco_pdf.make_paras(
"<para>"
+ (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,
}
+ "</para>",
style_bullet,
)
objects += sco_pdf.make_paras(
"""<para>Le jury propose les décisions suivantes :</para>""", style
)
objects += [Spacer(0, 4 * mm)]
if formsemestre.formation.is_apc():
rows, titles = jury_but_pv.pvjury_table_but(
formsemestre, etudids=etudids, line_sep="<br/>"
)
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"<para><b>{x}</b></para>" 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"""<para spaceBefore="10mm" align="right">{
sco_preferences.get_preference("DirectorName", formsemestre.id) or ""
}, {
sco_preferences.get_preference("DirectorTitle", formsemestre.id) or ""
}</para>""",
style,
)
# Légende des codes
codes = list(codes_cursus.CODES_EXPL.keys())
codes.sort()
objects += sco_pdf.make_paras(
"""<para spaceBefore="15mm" fontSize="14">
<b>Codes utilisés :</b></para>""",
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

View File

@ -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"""<para>{preferences["INSTITUTION_NAME"]}</para><para>{
preferences["INSTITUTION_ADDRESS"]}</para>""",
left_foot_style,
)
np = Paragraph(f'<para fontSize="14">{doc.page}</para>', 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

View File

@ -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"""<para>{preferences["INSTITUTION_NAME"]}</para><para>{
preferences["INSTITUTION_ADDRESS"]}</para>""",
left_foot_style,
)
np = Paragraph(f'<para fontSize="14">{doc.page}</para>', 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="<para>") -> str:
"""Reportlab bug turnaround (could be removed in a future version).
p is a string with Reportlab intra-paragraph XML tags.
Replaces <br> (currently ignored by Reportlab) by </para><para>
Also replaces <br> by <br/>
"""
return ("</para>" + para).join(
re.split(r"<.*?br.*?/>", paragraph_txt.replace("<br>", "<br/>"))
)
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", "<br/>")
# UE capitalisées:
if decision["decisions_ue"] and decision["decisions_ue_descr"]:
params["decision_ue_txt"] = (
"""<b>Unité%(decisions_ue_descr_plural)s d'Enseignement %(decision_orig)s capitalisée%(decisions_ue_descr_plural)s : %(decisions_ue_descr)s</b>"""
% params
)
else:
params["decision_ue_txt"] = ""
# Mention
params["mention"] = decision["mention"]
# Informations sur compensations
if decision["observation"]:
params["observation_txt"] = (
"""<b>Observation :</b> %(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 : <b>%s</b>""" % (
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 : <b>%(titre_formation)s</b>""" % 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, '<para leftindent="%(htab1)s">')
objects += sco_pdf.make_paras(
(
"""<para leftindent="%(htab1)s" spaceBefore="25mm">"""
+ sig
+ """</para>"""
)
% params,
style,
)
else:
sig = (
sco_preferences.get_preference(
"PV_LETTER_DIPLOMA_SIGNATURE", formsemestre_id
)
or ""
) % params
sig = _simulate_br(sig, '<para leftindent="%(htab1)s">')
objects += sco_pdf.make_paras(
(
"""<para leftindent="%(htab1)s" spaceBefore="25mm">"""
+ sig
+ """</para>"""
)
% 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"""<b>Décision du semestre antérieur S{params['prev_semestre_id']} :</b> {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"]}<br/>
<b>Niveaux de compétences:</b><br/> {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"""
<para align="center"><b>Procès-verbal de {titre_jury} du département {
sco_preferences.get_preference("DeptName", formsemestre.id) or "(sans nom)"
} - Session unique {formsemestre.annee_scolaire()}</b></para>
""",
style,
)
objects += sco_pdf.make_paras(
f"""<para align="center"><b><i>{titre_diplome}</i></b></para>""",
style,
)
if show_title:
objects += sco_pdf.make_paras(
f"""<para align="center"><b>Semestre: {formsemestre.titre}</b></para>""",
style,
)
if sco_preferences.get_preference("PV_TITLE_WITH_VDI", formsemestre.id):
objects += sco_pdf.make_paras(
f"""<para align="center">VDI et Code: {(code_vdi or "")}</para>""", style
)
if date_jury:
objects += sco_pdf.make_paras(
f"""<para align="center">Jury tenu le {date_jury}</para>""", style
)
objects += sco_pdf.make_paras(
"<para>"
+ (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,
}
+ "</para>",
style_bullet,
)
objects += sco_pdf.make_paras(
"""<para>Le jury propose les décisions suivantes :</para>""", style
)
objects += [Spacer(0, 4 * mm)]
if formsemestre.formation.is_apc():
rows, titles = jury_but_pv.pvjury_table_but(
formsemestre, etudids=etudids, line_sep="<br/>"
)
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"<para><b>{x}</b></para>" 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"""<para spaceBefore="10mm" align="right">{
sco_preferences.get_preference("DirectorName", formsemestre.id) or ""
}, {
sco_preferences.get_preference("DirectorTitle", formsemestre.id) or ""
}</para>""",
style,
)
# Légende des codes
codes = list(codes_cursus.CODES_EXPL.keys())
codes.sort()
objects += sco_pdf.make_paras(
"""<para spaceBefore="15mm" fontSize="14">
<b>Codes utilisés :</b></para>""",
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

View File

@ -54,7 +54,6 @@ from app.scodoc import sco_etud
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_pvjury
import sco_version import sco_version
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app import log from app import log

View File

@ -15,6 +15,19 @@
Information et documentation sur <a href="https://scodoc.org" target="_blank">scodoc.org</a>. Information et documentation sur <a href="https://scodoc.org" target="_blank">scodoc.org</a>.
</p> </p>
<p>Le logiciel est distribué sous
<a href="https://www.gnu.org/licenses/old-licenses/gpl-2.0.html">licence GNU
GPL v2</a>. <em>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.
</em>
</p>
<h2>Dernières évolutions</h2> <h2>Dernières évolutions</h2>
{{ news|safe }} {{ news|safe }}

View File

@ -122,12 +122,11 @@ from app.scodoc import sco_lycee
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
from app.scodoc import sco_moduleimpl_inscriptions from app.scodoc import sco_moduleimpl_inscriptions
from app.scodoc import sco_moduleimpl_status 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_placement
from app.scodoc import sco_poursuite_dut from app.scodoc import sco_poursuite_dut
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_prepajury 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_recapcomplet
from app.scodoc import sco_report from app.scodoc import sco_report
from app.scodoc import sco_report_but from app.scodoc import sco_report_but
@ -2803,7 +2802,9 @@ def formsemestre_validation_suppress_etud(
# ------------- PV de JURY et archives # ------------- 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) sco_publish("/pvjury_page_but", jury_but_pv.pvjury_page_but, Permission.ScoView)
@ -2913,12 +2914,12 @@ def formsemestre_jury_but_erase(
sco_publish( sco_publish(
"/formsemestre_lettres_individuelles", "/formsemestre_lettres_individuelles",
sco_pvjury.formsemestre_lettres_individuelles, sco_pv_forms.formsemestre_lettres_individuelles,
Permission.ScoView, Permission.ScoView,
methods=["GET", "POST"], methods=["GET", "POST"],
) )
sco_publish( 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( sco_publish(
"/feuille_preparation_jury", "/feuille_preparation_jury",

View File

@ -6,6 +6,15 @@ SCOVERSION = "9.4.45"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"
SCONEWS = """ SCONEWS = """
<h4>Année 2023</h4>
<ul>
<li>ScoDoc 9.4</li>
<ul>
<li>Améliorations des tableaux récapitulatifs</li>
<li>Nouvelle interface de gestions des groupes (S. Lehmann)</li>
<li>Enrichissement des jurys BUT et des procès-verbaux associés.</li>
</ul>
</ul>
<h4>Année 2022</h4> <h4>Année 2022</h4>
<ul> <ul>
<li>ScoDoc 9.4</li> <li>ScoDoc 9.4</li>
@ -14,7 +23,7 @@ SCONEWS = """
</ul> </ul>
<li>ScoDoc 9.3</li> <li>ScoDoc 9.3</li>
<ul> <ul>
<li>Nouvelle API REST pour connecter ScoDoc à d'autres applications<li> <li>Nouvelle API REST pour connecter ScoDoc à d'autres applications</li>
<li>Module de gestion des relations avec les entreprises</li> <li>Module de gestion des relations avec les entreprises</li>
<li>Prise en charge des parcours BUT</li> <li>Prise en charge des parcours BUT</li>
<li>Association des UEs aux compétences du référentiel</li> <li>Association des UEs aux compétences du référentiel</li>

View File

@ -29,7 +29,7 @@ from app.models import (
UniteEns, UniteEns,
) )
from app.scodoc import sco_utils as scu 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): 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) but_compare_decisions_annee(deca, deca_att)
if "autorisations_inscription" in doc_formsemestre["attendu"]: if "autorisations_inscription" in doc_formsemestre["attendu"]:
if dpv is None: # lazy load 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( check_autorisations_inscription(
etud, dpv, doc_formsemestre["attendu"]["autorisations_inscription"] etud, dpv, doc_formsemestre["attendu"]["autorisations_inscription"]
) )