ScoDoc-Lille/app/scodoc/sco_pv_lettres_inviduelles.py

362 lines
12 KiB
Python
Raw Normal View History

# -*- 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.
#
# 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
#
##############################################################################
"""Edition des lettres individuelles de jury
"""
# code initialement dans sco_pvpdf.py
import io
import re
from PIL import Image as PILImage
from PIL import UnidentifiedImageError
import reportlab
from reportlab.lib.units import cm, mm
from reportlab.lib.enums import TA_LEFT
from reportlab.platypus import PageBreak, Table, Image
from reportlab.platypus.doctemplate import BaseDocTemplate
from reportlab.lib import styles
from app import db
from app.models import FormSemestre, Identite
import app.scodoc.sco_utils as scu
from app.scodoc import sco_bulletins_pdf
from app.scodoc import sco_pv_dict
from app.scodoc import sco_pdf
from app.scodoc import sco_preferences
from app.scodoc.sco_exceptions import ScoPDFFormatError, ScoValueError
from app.scodoc.sco_cursus_dut import SituationEtudCursus
from app.scodoc.sco_pv_templates import CourrierIndividuelTemplate, jury_titres
import sco_version
def pdf_lettres_individuelles(
formsemestre_id,
etudids=None,
date_jury="",
date_commission="",
signature=None,
):
"""Document PDF avec les lettres d'avis pour les etudiants mentionnés
(tous ceux du semestre, ou la liste indiquée par etudids)
Renvoie pdf data ou chaine vide si aucun etudiant avec décision de jury.
"""
dpv = sco_pv_dict.dict_pvjury(formsemestre_id, etudids=etudids, with_prev=True)
if not dpv:
return ""
#
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
prefs = sco_preferences.SemPreferences(formsemestre_id)
params = {
"date_jury": date_jury,
"date_commission": date_commission,
"titre_formation": dpv["formation"]["titre_officiel"],
"htab1": "8cm", # lignes à droite (entete, signature)
"htab2": "1cm",
}
# copie preferences
for name in sco_preferences.get_base_preferences().prefs_name:
params[name] = sco_preferences.get_preference(name, formsemestre_id)
bookmarks = {}
objects = [] # list of PLATYPUS objects
npages = 0
for decision in dpv["decisions"]:
if (
decision["decision_sem"]
or decision.get("decision_annee")
or decision.get("decision_rcue")
or decision.get("decisions_ue")
): # decision prise
etud = Identite.get_etud(decision["identite"]["etudid"])
params["nomEtud"] = etud.nomprenom # backward compat
bookmarks[npages + 1] = scu.suppress_accents(etud.nomprenom)
try:
objects += pdf_lettre_individuelle(
dpv["formsemestre"], decision, etud, params, signature
)
except UnidentifiedImageError as exc:
raise ScoValueError(
"Fichier image (signature ou logo ?) invalide !"
) from exc
objects.append(PageBreak())
npages += 1
if npages == 0:
return ""
# Paramètres de mise en page
margins = (
prefs["left_margin"],
prefs["top_margin"],
prefs["right_margin"],
prefs["bottom_margin"],
)
# ----- Build PDF
report = io.BytesIO() # in-memory document, no disk file
document = BaseDocTemplate(report)
document.addPageTemplates(
CourrierIndividuelTemplate(
document,
author=f"{sco_version.SCONAME} {sco_version.SCOVERSION} (E. Viennet)",
title=f"Lettres décision {formsemestre.titre_annee()}",
subject="Décision jury",
margins=margins,
pagesbookmarks=bookmarks,
preferences=prefs,
)
)
try:
document.build(objects)
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
raise ScoPDFFormatError(str(exc)) from exc
data = report.getvalue()
return data
def _simulate_br(paragraph_txt: str, para="<para>") -> str:
"""Reportlab bug turnaround (could be removed in a future version).
p is a string with Reportlab intra-paragraph XML tags.
Replaces <br> (currently ignored by Reportlab) by </para><para>
Also replaces <br> by <br/>
"""
return ("</para>" + para).join(
re.split(r"<.*?br.*?/>", paragraph_txt.replace("<br>", "<br/>"))
)
def _make_signature_image(signature, leftindent, formsemestre_id) -> Table:
"crée un paragraphe avec l'image signature"
# cree une image PIL pour avoir la taille (W,H)
f = io.BytesIO(signature)
img = PILImage.open(f)
width, height = img.size
pdfheight = (
1.0
* sco_preferences.get_preference("pv_sig_image_height", formsemestre_id)
* mm
)
f.seek(0, 0)
style = styles.ParagraphStyle({})
style.leading = 1.0 * sco_preferences.get_preference(
"SCOLAR_FONT_SIZE", formsemestre_id
) # vertical space
style.leftIndent = leftindent
return Table(
[("", Image(f, width=width * pdfheight / float(height), height=pdfheight))],
colWidths=(9 * cm, 7 * cm),
)
def pdf_lettre_individuelle(sem, decision, etud: Identite, params, signature=None):
"""
Renvoie une liste d'objets PLATYPUS pour intégration
dans un autre document.
"""
#
formsemestre_id = sem["formsemestre_id"]
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
situation_etud: SituationEtudCursus = decision["Se"]
titre_jury, titre_jury_court = jury_titres(
formsemestre,
situation_etud.parcours_validated() or not situation_etud.semestre_non_terminal,
)
objects = []
style = reportlab.lib.styles.ParagraphStyle({})
style.fontSize = 14
style.fontName = sco_preferences.get_preference("PV_FONTNAME", formsemestre_id)
style.leading = 18
style.alignment = TA_LEFT
params["semestre_id"] = formsemestre.semestre_id
params["decision_sem_descr"] = decision["decision_sem_descr"]
params["type_jury"] = titre_jury # type de jury (passage ou delivrance)
params["type_jury_abbrv"] = titre_jury_court # idem, abbrégé
params["decisions_ue_descr"] = decision["decisions_ue_descr"]
if decision["decisions_ue_nb"] > 1:
params["decisions_ue_descr_plural"] = "s"
else:
params["decisions_ue_descr_plural"] = ""
params["INSTITUTION_CITY"] = (
sco_preferences.get_preference("INSTITUTION_CITY", formsemestre_id) or ""
)
if decision["prev_decision_sem"]:
params["prev_semestre_id"] = decision["prev"]["semestre_id"]
params["prev_decision_sem_txt"] = ""
params["decision_orig"] = ""
params.update(decision["identite"])
# fix domicile
if params.get("domicile"):
params["domicile"] = params["domicile"].replace("\\n", "<br/>")
# UE capitalisées:
if decision["decisions_ue"] and decision["decisions_ue_descr"]:
params["decision_ue_txt"] = (
"""<b>Unité%(decisions_ue_descr_plural)s d'Enseignement %(decision_orig)s capitalisée%(decisions_ue_descr_plural)s : %(decisions_ue_descr)s</b>"""
% params
)
else:
params["decision_ue_txt"] = ""
# Mention
params["mention"] = decision["mention"]
# Informations sur compensations
if decision["observation"]:
params["observation_txt"] = (
"""<b>Observation :</b> %(observation)s.""" % decision
)
else:
params["observation_txt"] = ""
# Autorisations de passage
if decision["autorisations"] and not situation_etud.parcours_validated():
if len(decision["autorisations"]) > 1:
titre_jury_court = "s"
else:
titre_jury_court = ""
params["autorisations_txt"] = (
"""Vous êtes autorisé%s à continuer dans le%s semestre%s : <b>%s</b>"""
% (
etud.e,
titre_jury_court,
titre_jury_court,
decision["autorisations_descr"],
)
)
else:
params["autorisations_txt"] = ""
if decision["decision_sem"] and situation_etud.parcours_validated():
params["diplome_txt"] = (
"""Vous avez donc obtenu le diplôme : <b>%(titre_formation)s</b>""" % params
)
else:
params["diplome_txt"] = ""
# Les fonctions ci-dessous ajoutent ou modifient des champs:
if formsemestre.formation.is_apc():
# ajout champs spécifiques PV BUT
add_apc_infos(formsemestre, params, decision)
else:
# ajout champs spécifiques PV DUT
add_classic_infos(formsemestre, params, decision)
# Corps de la lettre:
objects += sco_bulletins_pdf.process_field(
sco_preferences.get_preference("PV_LETTER_TEMPLATE", sem["formsemestre_id"]),
params,
style,
suppress_empty_pars=True,
)
# Signature:
# nota: si semestre terminal, signature par directeur IUT, sinon, signature par
# chef de département.
if situation_etud.semestre_non_terminal:
sig = (
sco_preferences.get_preference(
"PV_LETTER_PASSAGE_SIGNATURE", formsemestre_id
)
or ""
) % params
sig = _simulate_br(sig, '<para leftindent="%(htab1)s">')
objects += sco_pdf.make_paras(
(
"""<para leftindent="%(htab1)s" spaceBefore="25mm">"""
+ sig
+ """</para>"""
)
% params,
style,
)
else:
sig = (
sco_preferences.get_preference(
"PV_LETTER_DIPLOMA_SIGNATURE", formsemestre_id
)
or ""
) % params
sig = _simulate_br(sig, '<para leftindent="%(htab1)s">')
objects += sco_pdf.make_paras(
(
"""<para leftindent="%(htab1)s" spaceBefore="25mm">"""
+ sig
+ """</para>"""
)
% params,
style,
)
if signature:
try:
objects.append(
_make_signature_image(signature, params["htab1"], formsemestre_id)
)
except UnidentifiedImageError as exc:
raise ScoValueError("Image signature invalide !") from exc
return objects
def add_classic_infos(formsemestre: FormSemestre, params: dict, decision: dict):
"""Ajoute les champs pour les formations classiques, donc avec codes semestres"""
if decision["prev_decision_sem"]:
params["prev_code_descr"] = decision["prev_code_descr"]
params[
"prev_decision_sem_txt"
] = f"""<b>Décision du semestre antérieur S{params['prev_semestre_id']} :</b> {
params['prev_code_descr']}"""
# Décision semestre courant:
if formsemestre.semestre_id >= 0:
params["decision_orig"] = f"du semestre S{formsemestre.semestre_id}"
else:
params["decision_orig"] = ""
def add_apc_infos(formsemestre: FormSemestre, params: dict, decision: dict):
"""Ajoute les champs pour les formations APC (BUT), donc avec codes RCUE et année"""
annee_but = (formsemestre.semestre_id + 1) // 2
params["decision_orig"] = f"année BUT{annee_but}"
if decision is None:
params["decision_sem_descr"] = ""
params["decision_ue_txt"] = ""
else:
decision_annee = decision.get("decision_annee") or {}
params["decision_sem_descr"] = decision_annee.get("code") or ""
params[
"decision_ue_txt"
] = f"""{params["decision_ue_txt"]}<br/>
<b>Niveaux de compétences:</b><br/> {decision.get("descr_decisions_rcue") or ""}
"""