ScoDoc-PE/app/scodoc/sco_bulletins_generator.py

388 lines
14 KiB
Python

# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2024 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@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)
.bul_signatures_pdf()
.__init__ et .generate(fmt) methodes appelees par le client (sco_bulletin)
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
import io
import time
import traceback
import reportlab
from reportlab.platypus import (
DocIf,
Paragraph,
PageBreak,
)
from reportlab.platypus import Table, KeepInFrame
from flask import request
from flask_login import current_user
from app.models import FormSemestre, Identite, ScoDocSiteConfig
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import NoteProcessError
from app import log
from app.scodoc import sco_formsemestre
from app.scodoc import sco_pdf
from app.scodoc.sco_pdf import PDFLOCK
import sco_version
class BulletinGenerator:
"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
list_in_menu = True # la classe doit-elle est montrée dans le menu de config ?
scale_table_in_page = True # rescale la table sur 1 page
multi_pages = False
def __init__(
self,
bul_dict,
authuser=None,
etud: Identite = None,
filigranne=None,
formsemestre: FormSemestre = None,
server_name=None,
version="long",
with_img_signatures_pdf: bool = True,
):
from app.scodoc import sco_preferences
if version not in scu.BULLETINS_VERSIONS:
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
self.filigranne = filigranne
self.formsemestre: FormSemestre = formsemestre
self.server_name = server_name
self.version = version
self.with_img_signatures_pdf = with_img_signatures_pdf
# Store preferences for convenience:
formsemestre_id = self.bul_dict["formsemestre_id"]
self.preferences = sco_preferences.SemPreferences(formsemestre_id)
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
# - 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")
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})")
try:
PDFLOCK.acquire() # this lock is necessary since reportlab is not re-entrant
if fmt == "html":
return self.generate_html()
elif fmt == "pdf":
return self.generate_pdf(stand_alone=stand_alone)
else:
raise ValueError(f"invalid bulletin format ({fmt})")
finally:
PDFLOCK.release()
def generate_html(self):
"""Return bulletin as an HTML string"""
H = ['<div class="notes_bulletin">']
# table des notes:
H.append(self.bul_table(fmt="html")) # pylint: disable=no-member
# infos sous la table:
H.append(self.bul_part_below(fmt="html")) # pylint: disable=no-member
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"]
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 = []
# partie haute du bulletin
story += self.bul_title_pdf() # pylint: disable=no-member
index_obj_debut = len(story)
# table des notes
story += self.bul_table(fmt="pdf") # pylint: disable=no-member
# infos sous la table
story += self.bul_part_below(fmt="pdf") # pylint: disable=no-member
# signatures
story += self.bul_signatures_pdf() # pylint: disable=no-member
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>")]
#
# objects.append(sco_pdf.FinBulletin())
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),
)
)
document.build(story)
data = report.getvalue()
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
Pt = [
[Paragraph(sco_pdf.SU(x), self.CellStyle) for x in line] for line in P
]
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>"
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,
version=None, # short, long, selectedevals
fmt="pdf", # html, pdf
stand_alone=True,
with_img_signatures_pdf: bool = True,
):
"""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
version = version or "long"
if version not in scu.BULLETINS_VERSIONS:
raise ValueError("invalid version code !")
formsemestre_id = bul_dict["formsemestre_id"]
bul_class_name = sco_preferences.get_preference("bul_class_name", formsemestre_id)
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
if gen_class is None:
raise ValueError(f"Type de bulletin PDF invalide (paramètre: {bul_class_name})")
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,
with_img_signatures_pdf=with_img_signatures_pdf,
)
if fmt not in bul_generator.supported_formats:
# use standard generator
log(
"Bulletin format %s not supported by %s, using %s"
% (fmt, bul_class_name, bulletin_default_class_name())
)
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,
with_img_signatures_pdf=with_img_signatures_pdf,
)
data = bul_generator.generate(fmt=fmt, stand_alone=stand_alone)
finally:
PDFLOCK.release()
if bul_generator.diagnostic:
log(f"bul_error: {bul_generator.diagnostic}")
raise NoteProcessError(bul_generator.diagnostic)
filename = bul_generator.get_filename()
return data, filename
####
# 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():
return [
BULLETIN_CLASSES[class_name].description
for class_name in BULLETIN_CLASSES
if BULLETIN_CLASSES[class_name].list_in_menu
]
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