forked from ScoDoc/ScoDoc
315 lines
10 KiB
Python
315 lines
10 KiB
Python
# -*- mode: python -*-
|
|
# -*- coding: utf-8 -*-
|
|
|
|
##############################################################################
|
|
#
|
|
# Gestion scolarite IUT
|
|
#
|
|
# Copyright (c) 1999 - 2023 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@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 où 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
|
|
import time
|
|
import traceback
|
|
|
|
from flask import g, request
|
|
|
|
from app import log, ScoValueError
|
|
from app.models import FormSemestre
|
|
|
|
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
|
|
import app.scodoc.sco_utils as scu
|
|
|
|
import sco_version
|
|
|
|
|
|
def assemble_bulletins_pdf(
|
|
formsemestre_id: int,
|
|
story: list,
|
|
bul_title: str,
|
|
infos,
|
|
pagesbookmarks=None,
|
|
filigranne=None,
|
|
server_name="",
|
|
):
|
|
"Generate PDF document from a story (list of PLATYPUS objects)."
|
|
if not story:
|
|
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),
|
|
)
|
|
report = io.BytesIO() # in-memory document, no disk file
|
|
document = sco_pdf.BulletinDocTemplate(report)
|
|
document.addPageTemplates(
|
|
sco_pdf.ScoDocPageTemplate(
|
|
document,
|
|
author="%s %s (E. Viennet)" % (sco_version.SCONAME, sco_version.SCOVERSION),
|
|
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),
|
|
)
|
|
)
|
|
document.multiBuild(story)
|
|
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))
|
|
)
|
|
|
|
|
|
class WrapDict(object):
|
|
"""Wrap a dict so that getitem returns '' when values are None"""
|
|
|
|
def __init__(self, adict, NoneValue=""):
|
|
self.dict = adict
|
|
self.NoneValue = NoneValue
|
|
|
|
def __getitem__(self, key):
|
|
try:
|
|
value = self.dict[key]
|
|
except KeyError:
|
|
return f"XXX {key} invalide XXX"
|
|
if value is None:
|
|
return self.NoneValue
|
|
else:
|
|
return value
|
|
|
|
|
|
def process_field(field, cdict, style, suppress_empty_pars=False, format="pdf"):
|
|
"""Process a field given in preferences, returns
|
|
- if format = 'pdf': a list of Platypus objects
|
|
- if format = 'html' : a string
|
|
|
|
Substitutes all %()s markup
|
|
Remove potentialy harmful <img> tags
|
|
Replaces <logo name="header" width="xxx" height="xxx">
|
|
by <img src=".../logos/logo_header" width="xxx" height="xxx">
|
|
|
|
If format = 'html', replaces <para> by <p>. HTML does not allow logos.
|
|
"""
|
|
try:
|
|
# None values are mapped to empty strings by WrapDict
|
|
text = (field or "") % WrapDict(cdict)
|
|
except KeyError as exc:
|
|
missing_key = exc.args[0] if len(exc.args) > 0 else "?"
|
|
log(
|
|
f"""process_field: KeyError {missing_key} on field={field!r}
|
|
values={pprint.pformat(cdict)}
|
|
"""
|
|
)
|
|
text = f"""<para><i>format invalide: champs</i> {missing_key} <i>inexistant !</i></para>"""
|
|
scu.flash_once(
|
|
f"Attention: format PDF invalide (champs {field}, clef {missing_key})"
|
|
)
|
|
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})")
|
|
text = (
|
|
"<para><i>format invalide !</i></para><para>"
|
|
+ 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
|
|
|
|
# log('field: %s' % (text))
|
|
return sco_pdf.make_paras(text, style, suppress_empty=suppress_empty_pars)
|
|
|
|
|
|
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)
|
|
if cached:
|
|
return cached[1], cached[0]
|
|
fragments = []
|
|
# Make each bulletin
|
|
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
|
for etud in formsemestre.get_inscrits(include_demdef=True, order=True):
|
|
frag, _ = sco_bulletins.do_formsemestre_bulletinetud(
|
|
formsemestre,
|
|
etud.id,
|
|
format="pdfpart",
|
|
version=version,
|
|
)
|
|
fragments += frag
|
|
#
|
|
infos = {"DeptName": sco_preferences.get_preference("DeptName", formsemestre_id)}
|
|
if request:
|
|
server_name = request.url_root
|
|
else:
|
|
server_name = ""
|
|
try:
|
|
sco_pdf.PDFLOCK.acquire()
|
|
pdfdoc = assemble_bulletins_pdf(
|
|
formsemestre_id,
|
|
fragments,
|
|
formsemestre.titre_mois(),
|
|
infos,
|
|
server_name=server_name,
|
|
)
|
|
finally:
|
|
sco_pdf.PDFLOCK.release()
|
|
#
|
|
date_iso = time.strftime("%Y-%m-%d")
|
|
filename = "bul-%s-%s.pdf" % (formsemestre.titre_num(), date_iso)
|
|
filename = scu.unescape_html(filename).replace(" ", "_").replace("&", "")
|
|
# fill cache
|
|
sco_cache.SemBulletinsPDFCache.set(
|
|
str(formsemestre_id) + "_" + version, (filename, pdfdoc)
|
|
)
|
|
return pdfdoc, filename
|
|
|
|
|
|
def get_etud_bulletins_pdf(etudid, version="selectedevals"):
|
|
"Bulletins pdf de tous les semestres de l'étudiant, et filename"
|
|
from app.scodoc import sco_bulletins
|
|
|
|
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
|
fragments = []
|
|
bookmarks = {}
|
|
filigrannes = {}
|
|
i = 1
|
|
for sem in etud["sems"]:
|
|
formsemestre = FormSemestre.query.get(sem["formsemestre_id"])
|
|
frag, filigranne = sco_bulletins.do_formsemestre_bulletinetud(
|
|
formsemestre,
|
|
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
|
|
else:
|
|
server_name = ""
|
|
try:
|
|
sco_pdf.PDFLOCK.acquire()
|
|
pdfdoc = assemble_bulletins_pdf(
|
|
None,
|
|
fragments,
|
|
etud["nomprenom"],
|
|
infos,
|
|
bookmarks,
|
|
filigranne=filigrannes,
|
|
server_name=server_name,
|
|
)
|
|
finally:
|
|
sco_pdf.PDFLOCK.release()
|
|
#
|
|
filename = "bul-%s" % (etud["nomprenom"])
|
|
filename = (
|
|
scu.unescape_html(filename).replace(" ", "_").replace("&", "").replace(".", "")
|
|
+ ".pdf"
|
|
)
|
|
|
|
return pdfdoc, filename
|
|
|
|
|
|
def get_filigranne(etud_etat: str, prefs, decision_sem=None) -> str:
|
|
"""Texte à placer en "filigranne" sur le bulletin pdf"""
|
|
if etud_etat == scu.DEMISSION:
|
|
return "Démission"
|
|
elif etud_etat == codes_cursus.DEF:
|
|
return "Défaillant"
|
|
elif (prefs["bul_show_temporary"] and not decision_sem) or prefs[
|
|
"bul_show_temporary_forced"
|
|
]:
|
|
return prefs["bul_temporary_txt"]
|
|
return ""
|