ScoDoc-Lille/app/scodoc/sco_bulletins_generator.py

336 lines
12 KiB
Python

# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 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(format)
.bul_part_below(format)
.bul_signatures_pdf()
.__init__ et .generate(format) 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 SimpleDocTemplate, Paragraph, Spacer, Frame, PageBreak
from reportlab.platypus import Table, TableStyle, Image, KeepInFrame
from flask import request
from flask_login import current_user
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 ?
def __init__(
self,
infos,
authuser=None,
version="long",
filigranne=None,
server_name=None,
):
from app.scodoc import sco_preferences
if not version in scu.BULLETINS_VERSIONS:
raise ValueError("invalid version code !")
self.infos = infos
self.authuser = authuser # nécessaire pour version HTML qui contient liens dépendant de l'utilisateur
self.version = version
self.filigranne = filigranne
self.server_name = server_name
# Store preferences for convenience:
formsemestre_id = self.infos["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.FieldStyle = reportlab.lib.styles.ParagraphStyle({})
self.FieldStyle.fontName = self.preferences["SCOLAR_FONT_BUL_FIELDS"]
self.FieldStyle.fontSize = self.preferences["SCOLAR_FONT_SIZE"]
self.FieldStyle.firstLineIndent = 0
# - 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.infos["formsemestre_id"])
return scu.bul_filename_old(sem, self.infos["etud"], "pdf")
def generate(self, format="", stand_alone=True):
"""Return bulletin in specified format"""
if not format in self.supported_formats:
raise ValueError("unsupported bulletin format (%s)" % format)
try:
PDFLOCK.acquire() # this lock is necessary since reportlab is not re-entrant
if format == "html":
return self.generate_html()
elif format == "pdf":
return self.generate_pdf(stand_alone=stand_alone)
else:
raise ValueError("invalid bulletin format (%s)" % format)
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(format="html")) # pylint: disable=no-member
# infos sous la table:
H.append(self.bul_part_below(format="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.infos["formsemestre_id"]
# partie haute du bulletin
objects = self.bul_title_pdf() # pylint: disable=no-member
# table des notes
objects += self.bul_table(format="pdf") # pylint: disable=no-member
# infos sous la table
objects += self.bul_part_below(format="pdf") # pylint: disable=no-member
# signatures
objects += self.bul_signatures_pdf() # pylint: disable=no-member
# Réduit sur une page
objects = [KeepInFrame(0, 0, objects, mode="shrink")]
#
if not stand_alone:
objects.append(PageBreak()) # insert page break at end
return objects
else:
# 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.ScolarsPageTemplate(
document,
author="%s %s (E. Viennet) [%s]"
% (sco_version.SCONAME, sco_version.SCOVERSION, self.description),
title="Bulletin %s de %s"
% (sem["titremois"], self.infos["etud"]["nomprenom"]),
subject="Bulletin de note",
margins=self.margins,
server_name=self.server_name,
filigranne=self.filigranne,
preferences=sco_preferences.SemPreferences(formsemestre_id),
)
)
document.build(objects)
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_bulletinetud(
infos,
version="long", # short, long, selectedevals
format="pdf", # html, pdf
stand_alone=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
if not version in scu.BULLETINS_VERSIONS:
raise ValueError("invalid version code !")
formsemestre_id = infos["formsemestre_id"]
bul_class_name = sco_preferences.get_preference("bul_class_name", formsemestre_id)
gen_class = None
if infos.get("type") == "BUT" and format.startswith("pdf"):
gen_class = bulletin_get_class(bul_class_name + "BUT")
if gen_class is None:
gen_class = bulletin_get_class(bul_class_name)
if gen_class is None:
raise ValueError(
"Type de bulletin PDF invalide (paramètre: %s)" % bul_class_name
)
try:
PDFLOCK.acquire()
bul_generator = gen_class(
infos,
authuser=current_user,
version=version,
filigranne=infos["filigranne"],
server_name=request.url_root,
)
if format not in bul_generator.supported_formats:
# use standard generator
log(
"Bulletin format %s not supported by %s, using %s"
% (format, 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(
infos,
authuser=current_user,
version=version,
filigranne=infos["filigranne"],
server_name=request.url_root,
)
data = bul_generator.generate(format=format, stand_alone=stand_alone)
finally:
PDFLOCK.release()
if bul_generator.diagnostic:
log("bul_error: %s" % 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