ScoDoc/app/scodoc/sco_bulletins_generator.py

321 lines
12 KiB
Python
Raw Normal View History

2020-09-26 16:19:37 +02:00
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
2021-01-01 17:51:08 +01:00
# Copyright (c) 1999 - 2021 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(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
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 SimpleDocTemplate, Paragraph, Spacer, Frame, PageBreak
from reportlab.platypus import Table, TableStyle, Image, KeepInFrame
2021-07-19 19:53:01 +02:00
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import NoteProcessError
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
# Liste des types des classes de générateurs de bulletins PDF:
BULLETIN_CLASSES = collections.OrderedDict()
2020-09-26 16:19:37 +02:00
def register_bulletin_class(klass):
BULLETIN_CLASSES[klass.__name__] = klass
def bulletin_class_descriptions():
return [x.description for x in BULLETIN_CLASSES.values()]
def bulletin_class_names():
2021-07-09 17:47:06 +02:00
return list(BULLETIN_CLASSES.keys())
2020-09-26 16:19:37 +02:00
def bulletin_default_class_name():
return bulletin_class_names()[0]
def bulletin_get_class(class_name):
return BULLETIN_CLASSES[class_name]
def bulletin_get_class_name_displayed(formsemestre_id):
2020-09-26 16:19:37 +02:00
"""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)
2020-09-26 16:19:37 +02:00
try:
gen_class = bulletin_get_class(bul_class_name)
return gen_class.description
except:
return "invalide ! (voir paramètres)"
2021-07-09 23:31:16 +02:00
class BulletinGenerator(object):
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
def __init__(
self,
infos,
authuser=None,
version="long",
filigranne=None,
server_name=None,
):
from app.scodoc import sco_preferences
2021-07-19 19:53:01 +02:00
if not version in scu.BULLETINS_VERSIONS:
2020-09-26 16:19:37 +02:00
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)
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.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"""
2021-08-20 01:22:44 +02:00
sem = sco_formsemestre.get_formsemestre(self.infos["formsemestre_id"])
2020-09-26 16:19:37 +02:00
dt = time.strftime("%Y-%m-%d")
filename = "bul-%s-%s-%s.pdf" % (
sem["titre_num"],
dt,
self.infos["etud"]["nom"],
)
2021-07-19 19:53:01 +02:00
filename = scu.unescape_html(filename).replace(" ", "_").replace("&", "")
2020-09-26 16:19:37 +02:00
return filename
def generate(self, format="", stand_alone=True):
"""Return bulletin in specified format"""
2020-09-26 16:19:37 +02:00
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"""
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(format="html")) # pylint: disable=no-member
# infos sous la table:
H.append(self.bul_part_below(format="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
2020-09-26 16:19:37 +02:00
formsemestre_id = self.infos["formsemestre_id"]
2021-01-10 18:05:20 +01:00
# 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
2020-09-26 16:19:37 +02:00
# 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
2021-08-20 01:22:44 +02:00
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
2021-07-13 17:00:25 +02:00
report = io.BytesIO() # in-memory document, no disk file
2021-01-10 18:05:20 +01:00
document = sco_pdf.BaseDocTemplate(report)
2020-09-26 16:19:37 +02:00
document.addPageTemplates(
2021-01-10 18:05:20 +01:00
sco_pdf.ScolarsPageTemplate(
2020-09-26 16:19:37 +02:00
document,
author="%s %s (E. Viennet) [%s]"
2021-08-21 17:07:44 +02:00
% (sco_version.SCONAME, sco_version.SCOVERSION, self.description),
2020-09-26 16:19:37 +02:00
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),
2020-09-26 16:19:37 +02:00
)
)
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
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/>"
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,
REQUEST=None,
):
"""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
2021-07-19 19:53:01 +02:00
if not version in scu.BULLETINS_VERSIONS:
2020-09-26 16:19:37 +02:00
raise ValueError("invalid version code !")
formsemestre_id = infos["formsemestre_id"]
bul_class_name = sco_preferences.get_preference("bul_class_name", formsemestre_id)
2020-09-26 16:19:37 +02:00
try:
gen_class = bulletin_get_class(bul_class_name)
except:
raise ValueError(
"Type de bulletin PDF invalide (paramètre: %s)" % bul_class_name
2020-09-26 16:19:37 +02:00
)
try:
PDFLOCK.acquire()
bul_generator = gen_class(
infos,
authuser=REQUEST.AUTHENTICATED_USER,
version=version,
filigranne=infos["filigranne"],
server_name=REQUEST.BASE0,
)
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=REQUEST.AUTHENTICATED_USER,
version=version,
filigranne=infos["filigranne"],
server_name=REQUEST.BASE0,
)
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