ScoDoc-PE/app/scodoc/sco_bulletins_pdf.py

296 lines
9.9 KiB
Python
Raw Normal View History

2020-09-26 16:19:37 +02:00
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
2023-01-02 13:16:27 +01:00
# Copyright (c) 1999 - 2023 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@viennet.net
#
##############################################################################
"""Génération des bulletins de notes en format PDF
On peut installer plusieurs classes générant des bulletins de formats différents.
La préférence (par semestre) 'bul_pdf_class_name' conserve le nom de la classe Python
utilisée pour générer les bulletins en PDF. Elle doit être une sous-classe de PDFBulletinGenerator
et définir les méthodes fabriquant les éléments PDF:
gen_part_title
gen_table
gen_part_below
gen_signatures
Les éléments PDF sont des objets PLATYPUS de la bibliothèque Reportlab.
Voir la documentation (Reportlab's User Guide), chapitre 5 et suivants.
Pour définir un nouveau type de bulletin:
- créer un fichier source sco_bulletins_pdf_xxxx.py xxxx est le nom (court) de votre type;
- dans ce fichier, sous-classer PDFBulletinGenerator ou PDFBulletinGeneratorDefault
(s'inspirer de sco_bulletins_pdf_default);
- en fin du fichier sco_bulletins_pdf.py, ajouter la ligne
import sco_bulletins_pdf_xxxx
- votre type sera alors (après redémarrage de ScoDoc) proposé dans le formulaire de paramètrage ScoDoc.
Chaque semestre peut si nécessaire utiliser un type de bulletin différent.
"""
import io
import pprint
import pydoc
import re
2021-02-01 16:23:11 +01:00
import time
import traceback
2020-09-26 16:19:37 +02:00
from flask import g, request
2021-08-21 15:17:14 +02:00
from app import log, ScoValueError
2022-02-12 22:57:46 +01:00
from app.models import FormSemestre
2021-07-19 19:53:01 +02:00
from app.scodoc import sco_cache
from app.scodoc import codes_cursus
from app.scodoc import sco_pdf
from app.scodoc import sco_preferences
from app.scodoc import sco_etud
from app.scodoc.sco_logos import find_logo
2022-02-12 22:57:46 +01:00
import app.scodoc.sco_utils as scu
import sco_version
2020-09-26 16:19:37 +02:00
def assemble_bulletins_pdf(
formsemestre_id: int,
story: list,
bul_title: str,
2020-09-26 16:19:37 +02:00
infos,
pagesbookmarks=None,
2020-09-26 16:19:37 +02:00
filigranne=None,
server_name="",
):
"Generate PDF document from a story (list of PLATYPUS objects)."
if not story:
2020-09-26 16:19:37 +02:00
return ""
# Paramètres de mise en page
margins = (
sco_preferences.get_preference("left_margin", formsemestre_id),
sco_preferences.get_preference("top_margin", formsemestre_id),
sco_preferences.get_preference("right_margin", formsemestre_id),
sco_preferences.get_preference("bottom_margin", formsemestre_id),
2020-09-26 16:19:37 +02:00
)
report = io.BytesIO() # in-memory document, no disk file
document = sco_pdf.BulletinDocTemplate(report)
2020-09-26 16:19:37 +02:00
document.addPageTemplates(
sco_pdf.ScoDocPageTemplate(
2020-09-26 16:19:37 +02:00
document,
2021-08-21 17:07:44 +02:00
author="%s %s (E. Viennet)" % (sco_version.SCONAME, sco_version.SCOVERSION),
2020-09-26 16:19:37 +02:00
title="Bulletin %s" % bul_title,
subject="Bulletin de note",
server_name=server_name,
margins=margins,
pagesbookmarks=pagesbookmarks,
filigranne=filigranne,
preferences=sco_preferences.SemPreferences(formsemestre_id),
2020-09-26 16:19:37 +02:00
)
)
document.multiBuild(story)
2020-09-26 16:19:37 +02:00
data = report.getvalue()
return data
def replacement_function(match):
balise = match.group(1)
name = match.group(3)
logo = find_logo(logoname=name, dept_id=g.scodoc_dept_id)
if logo is not None:
return r'<img %s src="%s"%s/>' % (match.group(2), logo.filepath, match.group(4))
raise ScoValueError(
'balise "%s": logo "%s" introuvable'
% (pydoc.html.escape(balise), pydoc.html.escape(name))
)
def process_field(field, cdict, style, suppress_empty_pars=False, format="pdf"):
2020-09-26 16:19:37 +02:00
"""Process a field given in preferences, returns
- if format = 'pdf': a list of Platypus objects
- if format = 'html' : a string
2020-10-21 22:56:25 +02:00
Substitutes all %()s markup
2020-09-26 16:19:37 +02:00
Remove potentialy harmful <img> tags
Replaces <logo name="header" width="xxx" height="xxx">
by <img src=".../logos/logo_header" width="xxx" height="xxx">
2020-10-21 22:56:25 +02:00
If format = 'html', replaces <para> by <p>. HTML does not allow logos.
2020-09-26 16:19:37 +02:00
"""
try:
2021-02-01 16:23:11 +01:00
text = (field or "") % scu.WrapDict(
2020-09-26 16:19:37 +02:00
cdict
) # note that None values are mapped to empty strings
except KeyError as exc:
log(
f"""process_field: KeyError on field={field!r}
values={pprint.pformat(cdict)}
"""
)
if len(exc.args) > 0:
missing_field = exc.args[0]
text = f"""<para><i>format invalide: champs</i> {missing_field} <i>inexistant !</i></para>"""
except: # pylint: disable=bare-except
log(
f"""process_field: invalid format. field={field!r}
values={pprint.pformat(cdict)}
"""
)
# ne sera pas visible si lien vers pdf:
scu.flash_once(f"Attention: format PDF invalide (champs {field}")
2020-09-26 16:19:37 +02:00
text = (
2022-02-09 00:36:50 +01:00
"<para><i>format invalide !</i></para><para>"
2020-09-26 16:19:37 +02:00
+ traceback.format_exc()
+ "</para>"
)
# remove unhandled or dangerous tags:
text = re.sub(r"<\s*img", "", text)
if format == "html":
# convert <para>
text = re.sub(r"<\s*para(\s*)(.*?)>", r"<p>", text)
return text
# --- PDF format:
# handle logos:
text = re.sub(
r"<(\s*)logo(.*?)src\s*=\s*(.*?)>", r"<\1logo\2\3>", text
) # remove forbidden src attribute
text = re.sub(
r'(<\s*logo(.*?)name\s*=\s*"(\w*?)"(.*?)/?>)',
replacement_function,
text,
)
# nota: le match sur \w*? donne le nom du logo et interdit les .. et autres
# tentatives d'acceder à d'autres fichiers !
# la protection contre des noms malveillants est aussi assurée par l'utilisation de
# secure_filename dans la classe Logo
2020-09-26 16:19:37 +02:00
# log('field: %s' % (text))
2022-11-20 23:03:26 +01:00
return sco_pdf.make_paras(text, style, suppress_empty=suppress_empty_pars)
2020-09-26 16:19:37 +02:00
def get_formsemestre_bulletins_pdf(formsemestre_id, version="selectedevals"):
"Document pdf avec tous les bulletins du semestre, et filename"
from app.scodoc import sco_bulletins
cached = sco_cache.SemBulletinsPDFCache.get(str(formsemestre_id) + "_" + version)
2020-09-26 16:19:37 +02:00
if cached:
return cached[1], cached[0]
fragments = []
# Make each bulletin
2022-02-12 22:57:46 +01:00
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
2022-02-13 13:58:09 +01:00
for etud in formsemestre.get_inscrits(include_demdef=True, order=True):
frag, _ = sco_bulletins.do_formsemestre_bulletinetud(
2022-02-14 23:21:42 +01:00
formsemestre,
2022-02-12 22:57:46 +01:00
etud.id,
2020-09-26 16:19:37 +02:00
format="pdfpart",
version=version,
)
fragments += frag
#
infos = {"DeptName": sco_preferences.get_preference("DeptName", formsemestre_id)}
if request:
server_name = request.url_root
2020-09-26 16:19:37 +02:00
else:
server_name = ""
try:
2021-02-01 16:23:11 +01:00
sco_pdf.PDFLOCK.acquire()
pdfdoc = assemble_bulletins_pdf(
2020-09-26 16:19:37 +02:00
formsemestre_id,
fragments,
2022-02-12 22:57:46 +01:00
formsemestre.titre_mois(),
2020-09-26 16:19:37 +02:00
infos,
server_name=server_name,
)
finally:
2021-02-01 16:23:11 +01:00
sco_pdf.PDFLOCK.release()
2020-09-26 16:19:37 +02:00
#
date_iso = time.strftime("%Y-%m-%d")
filename = "bul-%s-%s.pdf" % (formsemestre.titre_num(), date_iso)
2021-02-01 16:23:11 +01:00
filename = scu.unescape_html(filename).replace(" ", "_").replace("&", "")
2020-09-26 16:19:37 +02:00
# fill cache
2021-07-19 19:53:01 +02:00
sco_cache.SemBulletinsPDFCache.set(
str(formsemestre_id) + "_" + version, (filename, pdfdoc)
2021-07-19 19:53:01 +02:00
)
2020-09-26 16:19:37 +02:00
return pdfdoc, filename
def get_etud_bulletins_pdf(etudid, version="selectedevals"):
2020-09-26 16:19:37 +02:00
"Bulletins pdf de tous les semestres de l'étudiant, et filename"
from app.scodoc import sco_bulletins
2021-08-22 13:24:36 +02:00
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
2020-09-26 16:19:37 +02:00
fragments = []
bookmarks = {}
filigrannes = {}
i = 1
for sem in etud["sems"]:
2022-02-14 23:21:42 +01:00
formsemestre = FormSemestre.query.get(sem["formsemestre_id"])
2020-09-26 16:19:37 +02:00
frag, filigranne = sco_bulletins.do_formsemestre_bulletinetud(
2022-02-14 23:21:42 +01:00
formsemestre,
2020-09-26 16:19:37 +02:00
etudid,
format="pdfpart",
version=version,
)
fragments += frag
filigrannes[i] = filigranne
bookmarks[i] = sem["session_id"] # eg RT-DUT-FI-S1-2015
i = i + 1
infos = {"DeptName": sco_preferences.get_preference("DeptName")}
if request:
server_name = request.url_root
2020-09-26 16:19:37 +02:00
else:
server_name = ""
try:
2021-02-01 16:23:11 +01:00
sco_pdf.PDFLOCK.acquire()
pdfdoc = assemble_bulletins_pdf(
2020-09-26 16:19:37 +02:00
None,
fragments,
etud["nomprenom"],
infos,
bookmarks,
filigranne=filigrannes,
server_name=server_name,
)
finally:
2021-02-01 16:23:11 +01:00
sco_pdf.PDFLOCK.release()
2020-09-26 16:19:37 +02:00
#
filename = "bul-%s" % (etud["nomprenom"])
filename = (
2021-02-01 16:23:11 +01:00
scu.unescape_html(filename).replace(" ", "_").replace("&", "").replace(".", "")
2020-09-26 16:19:37 +02:00
+ ".pdf"
)
return pdfdoc, filename
2022-02-14 23:21:42 +01:00
2022-02-21 19:25:38 +01:00
def get_filigranne(etud_etat: str, prefs, decision_sem=None) -> str:
2022-02-14 23:21:42 +01:00
"""Texte à placer en "filigranne" sur le bulletin pdf"""
if etud_etat == scu.DEMISSION:
return "Démission"
elif etud_etat == codes_cursus.DEF:
2022-02-14 23:21:42 +01:00
return "Défaillant"
2022-02-21 19:25:38 +01:00
elif (prefs["bul_show_temporary"] and not decision_sem) or prefs[
2022-02-14 23:21:42 +01:00
"bul_show_temporary_forced"
]:
return prefs["bul_temporary_txt"]
return ""