ScoDoc-PE/app/scodoc/sco_bulletins_pdf.py

369 lines
12 KiB
Python
Raw Normal View History

2020-09-26 16:19:37 +02:00
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
2023-12-31 23:04:06 +01:00
# Copyright (c) 1999 - 2024 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.
2020-09-26 16:19:37 +02:00
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
2024-01-17 23:52:14 +01:00
from app.comp.res_but import ResultatsSemestreBUT
from app.models import FormSemestre, Identite
from app.scodoc import (
codes_cursus,
sco_cache,
sco_pdf,
sco_preferences,
)
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,
author=f"{sco_version.SCONAME} {sco_version.SCOVERSION} (E. Viennet)",
title=f"Bulletin {bul_title}",
2020-09-26 16:19:37 +02:00
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
2024-02-23 19:03:02 +01:00
def replacement_function(match) -> str:
"remplace logo par balise html img"
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
and non existent keys returns an error message as value.
"""
def __init__(self, adict, none_value=""):
self.dict = adict
self.none_value = none_value
def __getitem__(self, key):
try:
value = self.dict[key]
except KeyError:
return f"XXX {key} invalide XXX"
if value is None:
return self.none_value
return value
def process_field(
field, cdict, style, suppress_empty_pars=False, fmt="pdf", field_name=None
):
2020-09-26 16:19:37 +02:00
"""Process a field given in preferences, returns
- if fmt = 'pdf': a list of Platypus objects
- if fmt = '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">
If fmt = 'html', replaces <para> by <p>. HTML does not allow logos.
2020-09-26 16:19:37 +02:00
"""
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)}
"""
)
2024-02-12 10:12:46 +01:00
text = f"""<para><i>format invalide: champ</i> {missing_key} <i>inexistant !</i></para>"""
scu.flash_once(
2024-02-12 10:12:46 +01:00
f"Attention: format PDF invalide (champ {field}, clef {missing_key})"
)
2023-09-01 14:38:41 +02:00
raise
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 = (
"<para><i>format invalide ! (1)</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 fmt == "html":
2020-09-26 16:19:37 +02:00
# 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))
return sco_pdf.make_paras(
text, style, suppress_empty=suppress_empty_pars, field_name=field_name
)
2020-09-26 16:19:37 +02:00
def get_formsemestre_bulletins_pdf(
formsemestre_id,
version="selectedevals",
groups_infos=None, # si indiqué, ne prend que ces groupes
):
"Document pdf avec tous les bulletins du semestre, et filename"
2023-12-06 20:04:40 +01:00
from app.but import bulletin_but_court
from app.scodoc import sco_bulletins
2023-12-06 20:04:40 +01:00
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
versions = (
scu.BULLETINS_VERSIONS_BUT
if formsemestre.formation.is_apc()
else scu.BULLETINS_VERSIONS
)
if version not in versions:
raise ScoValueError(
"get_formsemestre_bulletins_pdf: version de bulletin demandée invalide !"
)
etuds = formsemestre.get_inscrits(include_demdef=True, order=True)
2024-02-23 19:03:02 +01:00
if groups_infos is None:
gr_key = ""
else:
etudids = {m["etudid"] for m in groups_infos.members}
etuds = [etud for etud in etuds if etud.id in etudids]
2024-02-23 19:03:02 +01:00
gr_key = groups_infos.get_groups_key()
cache_key = str(formsemestre_id) + "_" + version + "_" + gr_key
cached = sco_cache.SemBulletinsPDFCache.get(cache_key)
2020-09-26 16:19:37 +02:00
if cached:
return cached[1], cached[0]
fragments = []
# Make each bulletin
for etud in etuds:
2023-12-06 20:04:40 +01:00
if version == "butcourt":
frag = bulletin_but_court.bulletin_but_court_pdf_frag(etud, formsemestre)
else:
frag, _ = sco_bulletins.do_formsemestre_bulletinetud(
formsemestre,
etud,
fmt="pdfpart",
version=version,
)
2020-09-26 16:19:37 +02:00
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")
2024-02-23 19:03:02 +01:00
filename = f"bul-{formsemestre.titre_num()}-{date_iso}.pdf"
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
etud = Identite.get_etud(etudid)
2020-09-26 16:19:37 +02:00
fragments = []
bookmarks = {}
filigrannes = {}
i = 1
for formsemestre in etud.get_formsemestres():
2020-09-26 16:19:37 +02:00
frag, filigranne = sco_bulletins.do_formsemestre_bulletinetud(
2022-02-14 23:21:42 +01:00
formsemestre,
etud,
fmt="pdfpart",
2020-09-26 16:19:37 +02:00
version=version,
)
fragments += frag
filigrannes[i] = filigranne
bookmarks[i] = formsemestre.session_id() # eg RT-DUT-FI-S1-2015
2020-09-26 16:19:37 +02:00
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,
2020-09-26 16:19:37 +02:00
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 = f"bul-{etud.nomprenom}"
2020-09-26 16:19:37 +02:00
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
2024-01-17 23:52:14 +01:00
def get_filigranne(
etud_etat: str, prefs, decision_sem: str | None | bool = None
) -> str:
"""Texte à placer en "filigranne" sur le bulletin pdf.
etud_etat : etat de l'inscription (I ou D)
decision_sem = code jury ou vide
"""
2022-02-14 23:21:42 +01:00
if etud_etat == scu.DEMISSION:
return "Démission"
2024-01-17 23:52:14 +01:00
if etud_etat == codes_cursus.DEF:
2022-02-14 23:21:42 +01:00
return "Défaillant"
2024-01-17 23:52:14 +01:00
if (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 ""
2024-01-17 23:52:14 +01:00
def get_filigranne_apc(
etud_etat: str, prefs, etudid: int, res: ResultatsSemestreBUT
) -> str:
"""Texte à placer en "filigranne" sur le bulletin pdf.
Version optimisée pour BUT
"""
if prefs["bul_show_temporary_forced"]:
return get_filigranne(etud_etat, prefs)
if prefs["bul_show_temporary"]:
# requete les décisions de jury
decision_sem = res.etud_has_decision(etudid)
return get_filigranne(etud_etat, prefs, decision_sem=decision_sem)
return get_filigranne(etud_etat, prefs)