ScoDoc/app/scodoc/sco_bulletins_generator.py

397 lines
14 KiB
Python
Raw Permalink Normal View History

2020-09-26 16:19:37 +02:00
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
2023-12-31 23:04:06 +01:00
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
2020-09-26 16:19:37 +02:00
#
# 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@gmail.com
#
##############################################################################
"""Génération des bulletins de note: super-classe pour les générateurs (HTML et PDF)
class BulletinGenerator:
description
supported_formats = [ 'pdf', 'html' ]
.bul_title_pdf()
.bul_table(fmt)
.bul_part_below(fmt)
2020-09-26 16:19:37 +02:00
.bul_signatures_pdf()
.__init__ et .generate(fmt) methodes appelees par le client (sco_bulletin)
2020-09-26 16:19:37 +02:00
La préférence 'bul_class_name' donne le nom de la classe generateur.
La préférence 'bul_pdf_class_name' est obsolete (inutilisée).
"""
import collections
2021-07-13 17:00:25 +02:00
import io
import time
2021-01-10 18:05:20 +01:00
import traceback
2021-07-11 23:02:35 +02:00
2021-07-13 17:00:25 +02:00
2021-01-10 18:05:20 +01:00
import reportlab
from reportlab.platypus import (
DocIf,
Paragraph,
PageBreak,
)
from reportlab.platypus import Table, KeepInFrame
2021-01-10 18:05:20 +01:00
from flask import request
from flask_login import current_user
from app.models import FormSemestre, Identite, ScoDocSiteConfig
2021-07-19 19:53:01 +02:00
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import NoteProcessError, ScoPDFFormatError
2021-08-29 19:57:32 +02:00
from app import log
from app.scodoc import sco_formsemestre
from app.scodoc import sco_pdf
from app.scodoc.sco_pdf import PDFLOCK
2021-08-21 17:07:44 +02:00
import sco_version
2020-09-26 16:19:37 +02:00
2022-02-14 23:21:42 +01:00
class BulletinGenerator:
2020-09-26 16:19:37 +02:00
"Virtual superclass for PDF bulletin generators" ""
# Here some helper methods
# see sco_bulletins_standard.BulletinGeneratorStandard subclass for real methods
supported_formats = [] # should list supported formats, eg [ 'html', 'pdf' ]
description = "superclass for bulletins" # description for user interface
2022-02-21 19:25:38 +01:00
list_in_menu = True # la classe doit-elle est montrée dans le menu de config ?
2022-03-10 00:50:36 +01:00
scale_table_in_page = True # rescale la table sur 1 page
multi_pages = False
2020-09-26 16:19:37 +02:00
def __init__(
self,
bul_dict,
2020-09-26 16:19:37 +02:00
authuser=None,
etud: Identite = None,
2020-09-26 16:19:37 +02:00
filigranne=None,
formsemestre: FormSemestre = None,
2020-09-26 16:19:37 +02:00
server_name=None,
version="long",
2023-02-15 16:15:53 +01:00
with_img_signatures_pdf: bool = True,
2020-09-26 16:19:37 +02:00
):
from app.scodoc import sco_preferences
if version not in scu.BULLETINS_VERSIONS:
2020-09-26 16:19:37 +02:00
raise ValueError("invalid version code !")
self.bul_dict = bul_dict
self.infos = bul_dict # legacy code compat
# authuser nécessaire pour version HTML qui contient liens dépendants de l'utilisateur
self.authuser = authuser
self.etud: Identite = etud
2020-09-26 16:19:37 +02:00
self.filigranne = filigranne
self.formsemestre: FormSemestre = formsemestre
2020-09-26 16:19:37 +02:00
self.server_name = server_name
self.version = version
2023-02-15 16:15:53 +01:00
self.with_img_signatures_pdf = with_img_signatures_pdf
2020-09-26 16:19:37 +02:00
# Store preferences for convenience:
formsemestre_id = self.bul_dict["formsemestre_id"]
self.preferences = sco_preferences.SemPreferences(formsemestre_id)
2020-09-26 16:19:37 +02:00
self.diagnostic = None # error message if any problem
# Common PDF styles:
# - Pour tous les champs du bulletin sauf les cellules de table:
self.style_field = reportlab.lib.styles.ParagraphStyle({})
self.style_field.fontName = self.preferences["SCOLAR_FONT_BUL_FIELDS"]
self.style_field.fontSize = self.preferences["SCOLAR_FONT_SIZE"]
self.style_field.firstLineIndent = 0
# Champ signatures
self.style_signature = self.style_field
2020-09-26 16:19:37 +02:00
# - Pour les cellules de table:
self.CellStyle = reportlab.lib.styles.ParagraphStyle({})
self.CellStyle.fontSize = self.preferences["SCOLAR_FONT_SIZE"]
self.CellStyle.fontName = self.preferences["SCOLAR_FONT"]
self.CellStyle.leading = (
1.0 * self.preferences["SCOLAR_FONT_SIZE"]
) # vertical space
# Marges du document PDF
self.margins = (
self.preferences["left_margin"],
self.preferences["top_margin"],
self.preferences["right_margin"],
self.preferences["bottom_margin"],
)
def get_filename(self):
"""Build a filename to be proposed to the web client"""
sem = sco_formsemestre.get_formsemestre(self.bul_dict["formsemestre_id"])
return scu.bul_filename_old(sem, self.bul_dict["etud"], "pdf")
2020-09-26 16:19:37 +02:00
def generate(self, fmt="", stand_alone=True):
"""Return bulletin in specified format"""
if not fmt in self.supported_formats:
raise ValueError(f"unsupported bulletin format ({fmt})")
2020-09-26 16:19:37 +02:00
try:
PDFLOCK.acquire() # this lock is necessary since reportlab is not re-entrant
if fmt == "html":
2020-09-26 16:19:37 +02:00
return self.generate_html()
elif fmt == "pdf":
2020-09-26 16:19:37 +02:00
return self.generate_pdf(stand_alone=stand_alone)
else:
raise ValueError(f"invalid bulletin format ({fmt})")
2020-09-26 16:19:37 +02:00
finally:
PDFLOCK.release()
def generate_html(self):
"""Return bulletin as an HTML string"""
2020-09-26 16:19:37 +02:00
H = ['<div class="notes_bulletin">']
2021-01-10 18:05:20 +01:00
# table des notes:
H.append(self.bul_table(fmt="html")) # pylint: disable=no-member
2021-01-10 18:05:20 +01:00
# infos sous la table:
H.append(self.bul_part_below(fmt="html")) # pylint: disable=no-member
2020-09-26 16:19:37 +02:00
H.append("</div>")
return "\n".join(H)
def generate_pdf(self, stand_alone=True):
"""Build PDF bulletin from distinct parts
Si stand_alone, génère un doc PDF complet et renvoie une string
Sinon, renvoie juste une liste d'objets PLATYPUS pour intégration
dans un autre document.
"""
from app.scodoc import sco_preferences
formsemestre_id = self.bul_dict["formsemestre_id"]
nomprenom = self.bul_dict["etud"]["nomprenom"]
2023-03-13 06:39:36 +01:00
etat_civil = self.bul_dict["etud"]["etat_civil"]
marque_debut_bulletin = sco_pdf.DebutBulletin(
etat_civil,
filigranne=self.bul_dict["filigranne"],
footer_content=f"""ScoDoc - Bulletin de {nomprenom} - {time.strftime("%d/%m/%Y %H:%M")}""",
)
story = []
2021-01-10 18:05:20 +01:00
# partie haute du bulletin
story += self.bul_title_pdf() # pylint: disable=no-member
index_obj_debut = len(story)
2021-01-10 18:05:20 +01:00
# table des notes
story += self.bul_table(fmt="pdf") # pylint: disable=no-member
2021-01-10 18:05:20 +01:00
# infos sous la table
story += self.bul_part_below(fmt="pdf") # pylint: disable=no-member
2021-01-10 18:05:20 +01:00
# signatures
story += self.bul_signatures_pdf() # pylint: disable=no-member
2022-03-10 00:50:36 +01:00
if self.scale_table_in_page:
# Réduit sur une page
story = [marque_debut_bulletin, KeepInFrame(0, 0, story, mode="shrink")]
else:
# Insere notre marqueur qui permet de générer les bookmarks et filigrannes:
story.insert(index_obj_debut, marque_debut_bulletin)
if ScoDocSiteConfig.is_bul_pdf_disabled():
story = [Paragraph("<p>Export des PDF interdit par l'administrateur</p>")]
2020-09-26 16:19:37 +02:00
#
# objects.append(sco_pdf.FinBulletin())
2020-09-26 16:19:37 +02:00
if not stand_alone:
if self.multi_pages:
# Bulletins sur plusieurs page, force début suivant sur page impaire
story.append(
DocIf("doc.page%2 == 1", [PageBreak(), PageBreak()], [PageBreak()])
)
else:
story.append(PageBreak()) # insert page break at end
return story
# Generation du document PDF
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
report = io.BytesIO() # in-memory document, no disk file
document = sco_pdf.BaseDocTemplate(report)
document.addPageTemplates(
sco_pdf.ScoDocPageTemplate(
document,
author=f"""{sco_version.SCONAME} {
sco_version.SCOVERSION} (E. Viennet) [{self.description}]""",
title=f"""Bulletin {sem["titremois"]} de {etat_civil}""",
subject="Bulletin de note",
margins=self.margins,
server_name=self.server_name,
filigranne=self.filigranne,
preferences=sco_preferences.SemPreferences(formsemestre_id),
with_page_numbers=self.multi_pages,
2020-09-26 16:19:37 +02:00
)
)
try:
document.build(story)
except (
ValueError,
KeyError,
reportlab.platypus.doctemplate.LayoutError,
) as exc:
raise ScoPDFFormatError(str(exc)) from exc
data = report.getvalue()
2020-09-26 16:19:37 +02:00
return data
def buildTableObject(self, P, pdfTableStyle, colWidths):
"""Utility used by some old-style generators.
Build a platypus Table instance from a nested list of cells, style and widths.
P: table, as a list of lists
PdfTableStyle: commandes de style pour la table (reportlab)
"""
try:
# put each table cell in a Paragraph
2021-01-10 18:05:20 +01:00
Pt = [
[Paragraph(sco_pdf.SU(x), self.CellStyle) for x in line] for line in P
]
2020-09-26 16:19:37 +02:00
except:
# enquête sur exception intermittente...
log("*** bug in PDF buildTableObject:")
log("P=%s" % P)
# compris: reportlab is not thread safe !
# see http://two.pairlist.net/pipermail/reportlab-users/2006-June/005037.html
# (donc maintenant protégé dans ScoDoc par un Lock global)
self.diagnostic = "erreur lors de la génération du PDF<br>"
2020-09-26 16:19:37 +02:00
self.diagnostic += "<pre>" + traceback.format_exc() + "</pre>"
return []
return Table(Pt, colWidths=colWidths, style=pdfTableStyle)
# ---------------------------------------------------------------------------
def make_formsemestre_bulletin_etud(
bul_dict,
etud: Identite = None,
formsemestre: FormSemestre = None,
2022-03-10 00:50:36 +01:00
version=None, # short, long, selectedevals
fmt="pdf", # html, pdf
2020-09-26 16:19:37 +02:00
stand_alone=True,
2023-02-15 16:15:53 +01:00
with_img_signatures_pdf: bool = True,
2020-09-26 16:19:37 +02:00
):
"""Bulletin de notes
Appelle une fonction générant le bulletin au format spécifié à partir des informations infos,
selon les préférences du semestre.
"""
from app.scodoc import sco_preferences
2022-03-10 00:50:36 +01:00
version = version or "long"
if version not in scu.BULLETINS_VERSIONS:
2020-09-26 16:19:37 +02:00
raise ValueError("invalid version code !")
formsemestre_id = bul_dict["formsemestre_id"]
bul_class_name = sco_preferences.get_preference("bul_class_name", formsemestre_id)
2022-02-21 19:25:38 +01:00
gen_class = None
for bul_class_name in (
sco_preferences.get_preference("bul_class_name", formsemestre_id),
# si pas trouvé (modifs locales bizarres ,), ré-essaye avec la valeur par défaut
bulletin_default_class_name(),
):
if bul_dict.get("type") == "BUT" and fmt.startswith("pdf"):
gen_class = bulletin_get_class(bul_class_name + "BUT")
if gen_class is None and bul_dict.get("type") != "BUT":
gen_class = bulletin_get_class(bul_class_name)
if gen_class is not None:
break
2022-02-21 19:25:38 +01:00
if gen_class is None:
2023-02-15 16:15:53 +01:00
raise ValueError(f"Type de bulletin PDF invalide (paramètre: {bul_class_name})")
2020-09-26 16:19:37 +02:00
try:
PDFLOCK.acquire()
bul_generator = gen_class(
bul_dict,
authuser=current_user,
etud=etud,
filigranne=bul_dict["filigranne"],
formsemestre=formsemestre,
server_name=request.url_root,
version=version,
2023-02-15 16:15:53 +01:00
with_img_signatures_pdf=with_img_signatures_pdf,
2020-09-26 16:19:37 +02:00
)
if fmt not in bul_generator.supported_formats:
2020-09-26 16:19:37 +02:00
# use standard generator
log(
"Bulletin format %s not supported by %s, using %s"
% (fmt, bul_class_name, bulletin_default_class_name())
2020-09-26 16:19:37 +02:00
)
bul_class_name = bulletin_default_class_name()
gen_class = bulletin_get_class(bul_class_name)
bul_generator = gen_class(
bul_dict,
authuser=current_user,
etud=etud,
filigranne=bul_dict["filigranne"],
formsemestre=formsemestre,
server_name=request.url_root,
version=version,
2023-02-15 16:15:53 +01:00
with_img_signatures_pdf=with_img_signatures_pdf,
2020-09-26 16:19:37 +02:00
)
data = bul_generator.generate(fmt=fmt, stand_alone=stand_alone)
2020-09-26 16:19:37 +02:00
finally:
PDFLOCK.release()
if bul_generator.diagnostic:
log(f"bul_error: {bul_generator.diagnostic}")
2020-09-26 16:19:37 +02:00
raise NoteProcessError(bul_generator.diagnostic)
filename = bul_generator.get_filename()
return data, filename
2022-02-21 19:25:38 +01:00
####
# Liste des types des classes de générateurs de bulletins PDF:
BULLETIN_CLASSES = collections.OrderedDict()
def register_bulletin_class(klass):
BULLETIN_CLASSES[klass.__name__] = klass
def bulletin_class_descriptions():
2022-03-07 21:49:11 +01:00
return [
BULLETIN_CLASSES[class_name].description
for class_name in BULLETIN_CLASSES
if BULLETIN_CLASSES[class_name].list_in_menu
]
2022-02-21 19:25:38 +01:00
def bulletin_class_names() -> list[str]:
"Liste les noms des classes de bulletins à présenter à l'utilisateur"
return [
class_name
for class_name in BULLETIN_CLASSES
if BULLETIN_CLASSES[class_name].list_in_menu
]
def bulletin_default_class_name():
return bulletin_class_names()[0]
def bulletin_get_class(class_name: str) -> BulletinGenerator:
"""La class de génération de bulletin de ce nom,
ou None si pas trouvée
"""
return BULLETIN_CLASSES.get(class_name)
def bulletin_get_class_name_displayed(formsemestre_id):
"""Le nom du générateur utilisé, en clair"""
from app.scodoc import sco_preferences
bul_class_name = sco_preferences.get_preference("bul_class_name", formsemestre_id)
gen_class = bulletin_get_class(bul_class_name)
if gen_class is None:
return "invalide ! (voir paramètres)"
return gen_class.description