forked from ScoDoc/ScoDoc
371 lines
12 KiB
Python
371 lines
12 KiB
Python
# -*- mode: python -*-
|
|
# -*- coding: utf-8 -*-
|
|
|
|
##############################################################################
|
|
#
|
|
# Gestion scolarite IUT
|
|
#
|
|
# Copyright (c) 1999 - 2024 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.
|
|
|
|
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.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
|
|
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=f"{sco_version.SCONAME} {sco_version.SCOVERSION} (E. Viennet)",
|
|
title=f"Bulletin {bul_title}",
|
|
subject="Bulletin de note",
|
|
server_name=server_name,
|
|
margins=margins,
|
|
pagesbookmarks=pagesbookmarks,
|
|
filigranne=filigranne,
|
|
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
|
with_page_numbers=False, # on ne veut pas de no de pages sur les bulletins imprimés en masse
|
|
)
|
|
)
|
|
document.multiBuild(story)
|
|
data = report.getvalue()
|
|
return data
|
|
|
|
|
|
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)),
|
|
safe=True,
|
|
)
|
|
|
|
|
|
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
|
|
):
|
|
"""Process a field given in preferences, returns
|
|
- if fmt = 'pdf': a list of Platypus objects
|
|
- if fmt = '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 fmt = '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: champ</i> {missing_key} <i>inexistant !</i></para>"""
|
|
scu.flash_once(
|
|
f"Attention: format PDF invalide (champ {field}, clef {missing_key})"
|
|
)
|
|
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})")
|
|
text = (
|
|
"<para><i>format invalide ! (1)</i></para><para>"
|
|
+ traceback.format_exc()
|
|
+ "</para>"
|
|
)
|
|
# remove unhandled or dangerous tags:
|
|
text = re.sub(r"<\s*img", "", text)
|
|
if fmt == "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, field_name=field_name
|
|
)
|
|
|
|
|
|
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"
|
|
from app.but import bulletin_but_court
|
|
from app.scodoc import sco_bulletins
|
|
|
|
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)
|
|
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]
|
|
gr_key = groups_infos.get_groups_key()
|
|
|
|
cache_key = str(formsemestre_id) + "_" + version + "_" + gr_key
|
|
cached = sco_cache.SemBulletinsPDFCache.get(cache_key)
|
|
if cached:
|
|
return cached[1], cached[0]
|
|
fragments = []
|
|
# Make each bulletin
|
|
for etud in etuds:
|
|
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,
|
|
)
|
|
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 = f"bul-{formsemestre.titre_num()}-{date_iso}.pdf"
|
|
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 = Identite.get_etud(etudid)
|
|
fragments = []
|
|
bookmarks = {}
|
|
filigrannes = {}
|
|
i = 1
|
|
for formsemestre in etud.get_formsemestres():
|
|
frag, filigranne = sco_bulletins.do_formsemestre_bulletinetud(
|
|
formsemestre,
|
|
etud,
|
|
fmt="pdfpart",
|
|
version=version,
|
|
)
|
|
fragments += frag
|
|
filigrannes[i] = filigranne
|
|
bookmarks[i] = formsemestre.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 = f"bul-{etud.nomprenom}"
|
|
filename = (
|
|
scu.unescape_html(filename).replace(" ", "_").replace("&", "").replace(".", "")
|
|
+ ".pdf"
|
|
)
|
|
|
|
return pdfdoc, filename
|
|
|
|
|
|
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
|
|
"""
|
|
if etud_etat == scu.DEMISSION:
|
|
return "Démission"
|
|
if etud_etat == codes_cursus.DEF:
|
|
return "Défaillant"
|
|
if (prefs["bul_show_temporary"] and not decision_sem) or prefs[
|
|
"bul_show_temporary_forced"
|
|
]:
|
|
return prefs["bul_temporary_txt"]
|
|
return ""
|
|
|
|
|
|
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)
|