forked from ScoDoc/ScoDoc
943 lines
32 KiB
Python
943 lines
32 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
|
|
#
|
|
##############################################################################
|
|
|
|
"""Edition des PV de jury
|
|
"""
|
|
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, TA_RIGHT, TA_JUSTIFY
|
|
from reportlab.platypus import Paragraph, Spacer, Frame, PageBreak
|
|
from reportlab.platypus import Table, TableStyle, Image
|
|
from reportlab.platypus.doctemplate import PageTemplate, BaseDocTemplate
|
|
from reportlab.lib.pagesizes import A4, landscape
|
|
from reportlab.lib import styles
|
|
from reportlab.lib.colors import Color
|
|
|
|
from flask import g
|
|
from app.models import FormSemestre, Identite
|
|
|
|
import app.scodoc.sco_utils as scu
|
|
from app.scodoc import sco_bulletins_pdf
|
|
from app.scodoc import codes_cursus
|
|
from app.scodoc import sco_dict_pv_jury
|
|
from app.scodoc import sco_etud
|
|
from app.scodoc import sco_pdf
|
|
from app.scodoc import sco_preferences
|
|
from app.scodoc.sco_exceptions import ScoValueError
|
|
from app.scodoc.sco_logos import find_logo
|
|
from app.scodoc.sco_cursus_dut import SituationEtudCursus
|
|
from app.scodoc.sco_pdf import SU
|
|
import sco_version
|
|
|
|
LOGO_FOOTER_ASPECT = scu.CONFIG.LOGO_FOOTER_ASPECT # XXX A AUTOMATISER
|
|
LOGO_FOOTER_HEIGHT = scu.CONFIG.LOGO_FOOTER_HEIGHT * mm
|
|
LOGO_FOOTER_WIDTH = LOGO_FOOTER_HEIGHT * scu.CONFIG.LOGO_FOOTER_ASPECT
|
|
|
|
LOGO_HEADER_ASPECT = scu.CONFIG.LOGO_HEADER_ASPECT # XXX logo IUTV (A AUTOMATISER)
|
|
LOGO_HEADER_HEIGHT = scu.CONFIG.LOGO_HEADER_HEIGHT * mm
|
|
LOGO_HEADER_WIDTH = LOGO_HEADER_HEIGHT * scu.CONFIG.LOGO_HEADER_ASPECT
|
|
|
|
|
|
def page_footer(canvas, doc, logo, preferences, with_page_numbers=True):
|
|
"Add footer on page"
|
|
width = doc.pagesize[0] # - doc.pageTemplate.left_p - doc.pageTemplate.right_p
|
|
foot = Frame(
|
|
0.1 * mm,
|
|
0.2 * cm,
|
|
width - 1 * mm,
|
|
2 * cm,
|
|
leftPadding=0,
|
|
rightPadding=0,
|
|
topPadding=0,
|
|
bottomPadding=0,
|
|
id="monfooter",
|
|
showBoundary=0,
|
|
)
|
|
|
|
left_foot_style = reportlab.lib.styles.ParagraphStyle({})
|
|
left_foot_style.fontName = preferences["SCOLAR_FONT"]
|
|
left_foot_style.fontSize = preferences["SCOLAR_FONT_SIZE_FOOT"]
|
|
left_foot_style.leftIndent = 0
|
|
left_foot_style.firstLineIndent = 0
|
|
left_foot_style.alignment = TA_RIGHT
|
|
right_foot_style = reportlab.lib.styles.ParagraphStyle({})
|
|
right_foot_style.fontName = preferences["SCOLAR_FONT"]
|
|
right_foot_style.fontSize = preferences["SCOLAR_FONT_SIZE_FOOT"]
|
|
right_foot_style.alignment = TA_RIGHT
|
|
|
|
p = sco_pdf.make_paras(
|
|
f"""<para>{preferences["INSTITUTION_NAME"]}</para><para>{
|
|
preferences["INSTITUTION_ADDRESS"]}</para>""",
|
|
left_foot_style,
|
|
)
|
|
|
|
np = Paragraph(f'<para fontSize="14">{doc.page}</para>', right_foot_style)
|
|
tabstyle = TableStyle(
|
|
[
|
|
("LEFTPADDING", (0, 0), (-1, -1), 0),
|
|
("RIGHTPADDING", (0, 0), (-1, -1), 0),
|
|
("ALIGN", (0, 0), (-1, -1), "RIGHT"),
|
|
# ('INNERGRID', (0,0), (-1,-1), 0.25, black),#debug
|
|
# ('LINEABOVE', (0,0), (-1,0), 0.5, black),
|
|
("VALIGN", (1, 0), (1, 0), "MIDDLE"),
|
|
("RIGHTPADDING", (-1, 0), (-1, 0), 1 * cm),
|
|
]
|
|
)
|
|
elems = [p]
|
|
if logo:
|
|
elems.append(logo)
|
|
colWidths = [None, LOGO_FOOTER_WIDTH + 2 * mm]
|
|
if with_page_numbers:
|
|
elems.append(np)
|
|
colWidths.append(2 * cm)
|
|
else:
|
|
elems.append("")
|
|
colWidths.append(8 * mm) # force marge droite
|
|
tab = Table([elems], style=tabstyle, colWidths=colWidths)
|
|
canvas.saveState() # is it necessary ?
|
|
foot.addFromList([tab], canvas)
|
|
canvas.restoreState()
|
|
|
|
|
|
def page_header(canvas, doc, logo, preferences, only_on_first_page=False):
|
|
"Ajoute au canvas le frame avec le logo"
|
|
if only_on_first_page and int(doc.page) > 1:
|
|
return
|
|
height = doc.pagesize[1]
|
|
head = Frame(
|
|
-22 * mm,
|
|
height - 13 * mm - LOGO_HEADER_HEIGHT,
|
|
10 * cm,
|
|
LOGO_HEADER_HEIGHT + 2 * mm,
|
|
leftPadding=0,
|
|
rightPadding=0,
|
|
topPadding=0,
|
|
bottomPadding=0,
|
|
id="monheader",
|
|
showBoundary=0,
|
|
)
|
|
if logo:
|
|
canvas.saveState() # is it necessary ?
|
|
head.addFromList([logo], canvas)
|
|
canvas.restoreState()
|
|
|
|
|
|
class CourrierIndividuelTemplate(PageTemplate):
|
|
"""Template pour courrier avisant des decisions de jury (1 page par étudiant)"""
|
|
|
|
def __init__(
|
|
self,
|
|
document,
|
|
pagesbookmarks=None,
|
|
author=None,
|
|
title=None,
|
|
subject=None,
|
|
margins=(0, 0, 0, 0), # additional margins in mm (left,top,right, bottom)
|
|
preferences=None, # dictionnary with preferences, required
|
|
force_header=False,
|
|
force_footer=False, # always add a footer (whatever the preferences, use for PV)
|
|
template_name="CourrierJuryTemplate",
|
|
):
|
|
"""Initialise our page template."""
|
|
self.pagesbookmarks = pagesbookmarks or {}
|
|
self.pdfmeta_author = author
|
|
self.pdfmeta_title = title
|
|
self.pdfmeta_subject = subject
|
|
self.preferences = preferences
|
|
self.force_header = force_header
|
|
self.force_footer = force_footer
|
|
self.with_footer = (
|
|
self.force_footer or self.preferences["PV_LETTER_WITH_HEADER"]
|
|
)
|
|
self.with_header = (
|
|
self.force_header or self.preferences["PV_LETTER_WITH_FOOTER"]
|
|
)
|
|
self.with_page_background = self.preferences["PV_LETTER_WITH_BACKGROUND"]
|
|
self.with_page_numbers = False
|
|
self.header_only_on_first_page = False
|
|
# Our doc is made of a single frame
|
|
left, top, right, bottom = margins # marge additionnelle en mm
|
|
# marges du Frame principal
|
|
self.bot_p = 2 * cm
|
|
self.left_p = 2.5 * cm
|
|
self.right_p = 2.5 * cm
|
|
self.top_p = 0 * cm
|
|
# log("margins=%s" % str(margins))
|
|
content = Frame(
|
|
self.left_p + left * mm,
|
|
self.bot_p + bottom * mm,
|
|
document.pagesize[0] - self.right_p - self.left_p - left * mm - right * mm,
|
|
document.pagesize[1] - self.top_p - self.bot_p - top * mm - bottom * mm,
|
|
)
|
|
|
|
PageTemplate.__init__(self, template_name, [content])
|
|
|
|
self.background_image_filename = None
|
|
self.logo_footer = None
|
|
self.logo_header = None
|
|
# Search logos in dept specific dir, then in global scu.CONFIG dir
|
|
if template_name == "PVJuryTemplate":
|
|
background = find_logo(
|
|
logoname="pvjury_background",
|
|
dept_id=g.scodoc_dept_id,
|
|
) or find_logo(
|
|
logoname="pvjury_background",
|
|
dept_id=g.scodoc_dept_id,
|
|
prefix="",
|
|
)
|
|
else:
|
|
background = find_logo(
|
|
logoname="letter_background",
|
|
dept_id=g.scodoc_dept_id,
|
|
) or find_logo(
|
|
logoname="letter_background",
|
|
dept_id=g.scodoc_dept_id,
|
|
prefix="",
|
|
)
|
|
if not self.background_image_filename and background is not None:
|
|
self.background_image_filename = background.filepath
|
|
|
|
footer = find_logo(logoname="footer", dept_id=g.scodoc_dept_id)
|
|
if footer is not None:
|
|
self.logo_footer = Image(
|
|
footer.filepath,
|
|
height=LOGO_FOOTER_HEIGHT,
|
|
width=LOGO_FOOTER_WIDTH,
|
|
)
|
|
|
|
header = find_logo(logoname="header", dept_id=g.scodoc_dept_id)
|
|
if header is not None:
|
|
self.logo_header = Image(
|
|
header.filepath,
|
|
height=LOGO_HEADER_HEIGHT,
|
|
width=LOGO_HEADER_WIDTH,
|
|
)
|
|
|
|
def beforeDrawPage(self, canv, doc):
|
|
"""Draws a logo and an contribution message on each page."""
|
|
# ---- Add some meta data and bookmarks
|
|
if self.pdfmeta_author:
|
|
canv.setAuthor(SU(self.pdfmeta_author))
|
|
if self.pdfmeta_title:
|
|
canv.setTitle(SU(self.pdfmeta_title))
|
|
if self.pdfmeta_subject:
|
|
canv.setSubject(SU(self.pdfmeta_subject))
|
|
bm = self.pagesbookmarks.get(doc.page, None)
|
|
if bm != None:
|
|
key = bm
|
|
txt = SU(bm)
|
|
canv.bookmarkPage(key)
|
|
canv.addOutlineEntry(txt, bm)
|
|
|
|
# ---- Background image
|
|
if self.background_image_filename and self.with_page_background:
|
|
canv.drawImage(
|
|
self.background_image_filename, 0, 0, doc.pagesize[0], doc.pagesize[1]
|
|
)
|
|
|
|
# ---- Header/Footer
|
|
if self.with_header:
|
|
page_header(
|
|
canv,
|
|
doc,
|
|
self.logo_header,
|
|
self.preferences,
|
|
self.header_only_on_first_page,
|
|
)
|
|
if self.with_footer:
|
|
page_footer(
|
|
canv,
|
|
doc,
|
|
self.logo_footer,
|
|
self.preferences,
|
|
with_page_numbers=self.with_page_numbers,
|
|
)
|
|
|
|
|
|
class PVTemplate(CourrierIndividuelTemplate):
|
|
"""Template pour les pages des PV de jury"""
|
|
|
|
def __init__(
|
|
self,
|
|
document,
|
|
author=None,
|
|
title=None,
|
|
subject=None,
|
|
margins=None, # additional margins in mm (left,top,right, bottom)
|
|
preferences=None, # dictionnary with preferences, required
|
|
):
|
|
if margins is None:
|
|
margins = (
|
|
preferences["pv_left_margin"],
|
|
preferences["pv_top_margin"],
|
|
preferences["pv_right_margin"],
|
|
preferences["pv_bottom_margin"],
|
|
)
|
|
CourrierIndividuelTemplate.__init__(
|
|
self,
|
|
document,
|
|
author=author,
|
|
title=title,
|
|
subject=subject,
|
|
margins=margins,
|
|
preferences=preferences,
|
|
force_header=True,
|
|
force_footer=True,
|
|
template_name="PVJuryTemplate",
|
|
)
|
|
self.with_page_numbers = True
|
|
self.header_only_on_first_page = True
|
|
self.with_header = self.preferences["PV_WITH_HEADER"]
|
|
self.with_footer = self.preferences["PV_WITH_FOOTER"]
|
|
self.with_page_background = self.preferences["PV_WITH_BACKGROUND"]
|
|
|
|
def afterDrawPage(self, canv, doc):
|
|
"""Called after all flowables have been drawn on a page"""
|
|
pass
|
|
|
|
def beforeDrawPage(self, canv, doc):
|
|
"""Called before any flowables are drawn on a page"""
|
|
# If the page number is even, force a page break
|
|
CourrierIndividuelTemplate.beforeDrawPage(self, canv, doc)
|
|
# Note: on cherche un moyen de generer un saut de page double
|
|
# (redémarrer sur page impaire, nouvelle feuille en recto/verso). Pas trouvé en Platypus.
|
|
#
|
|
# if self.__pageNum % 2 == 0:
|
|
# canvas.showPage()
|
|
# # Increment pageNum again since we've added a blank page
|
|
# self.__pageNum += 1
|
|
|
|
|
|
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_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_dict_pv_jury.dict_pvjury(formsemestre_id, etudids=etudids, with_prev=True)
|
|
if not dpv:
|
|
return ""
|
|
# Ajoute infos sur etudiants
|
|
etuds = [x["identite"] for x in dpv["decisions"]]
|
|
sco_etud.fill_etuds_info(etuds)
|
|
#
|
|
formsemestre: FormSemestre = FormSemestre.query.get(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")
|
|
): # decision prise
|
|
etud: Identite = Identite.query.get(decision["identite"]["etudid"])
|
|
params["nomEtud"] = etud.nomprenom
|
|
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,
|
|
)
|
|
)
|
|
|
|
document.build(objects)
|
|
data = report.getvalue()
|
|
return data
|
|
|
|
|
|
def _descr_jury(formsemestre: FormSemestre, diplome):
|
|
|
|
if not diplome:
|
|
if formsemestre.formation.is_apc():
|
|
t = f"""BUT{(formsemestre.semestre_id+1)//2}"""
|
|
s = t
|
|
else:
|
|
t = f"""passage de Semestre {formsemestre.semestre_id} en Semestre {formsemestre.semestre_id + 1}"""
|
|
s = "passage de semestre"
|
|
else:
|
|
t = "délivrance du diplôme"
|
|
s = t
|
|
return t, s # titre long, titre court
|
|
|
|
|
|
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.query.get(formsemestre_id)
|
|
Se: SituationEtudCursus = decision["Se"]
|
|
t, s = _descr_jury(
|
|
formsemestre, Se.parcours_validated() or not Se.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"] = t # type de jury (passage ou delivrance)
|
|
params["type_jury_abbrv"] = s # 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["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 Se.parcours_validated():
|
|
if len(decision["autorisations"]) > 1:
|
|
s = "s"
|
|
else:
|
|
s = ""
|
|
params[
|
|
"autorisations_txt"
|
|
] = """Vous êtes autorisé%s à continuer dans le%s semestre%s : <b>%s</b>""" % (
|
|
etud.e,
|
|
s,
|
|
s,
|
|
decision["autorisations_descr"],
|
|
)
|
|
else:
|
|
params["autorisations_txt"] = ""
|
|
|
|
if decision["decision_sem"] and Se.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 Se.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 ""}
|
|
"""
|
|
|
|
|
|
# ----------------------------------------------
|
|
def pvjury_pdf(
|
|
formsemestre: FormSemestre,
|
|
etudids: list[int],
|
|
date_commission=None,
|
|
date_jury=None,
|
|
numero_arrete=None,
|
|
code_vdi=None,
|
|
show_title=False,
|
|
pv_title=None,
|
|
with_paragraph_nom=False,
|
|
anonymous=False,
|
|
) -> bytes:
|
|
"""Doc PDF récapitulant les décisions de jury
|
|
(tableau en format paysage)
|
|
"""
|
|
objects, a_diplome = _pvjury_pdf_type(
|
|
formsemestre,
|
|
etudids,
|
|
only_diplome=False,
|
|
date_commission=date_commission,
|
|
numero_arrete=numero_arrete,
|
|
code_vdi=code_vdi,
|
|
date_jury=date_jury,
|
|
show_title=show_title,
|
|
pv_title=pv_title,
|
|
with_paragraph_nom=with_paragraph_nom,
|
|
anonymous=anonymous,
|
|
)
|
|
if not objects:
|
|
return b""
|
|
|
|
jury_de_diplome = formsemestre.est_terminal()
|
|
|
|
# Si Jury de passage et qu'un étudiant valide le parcours
|
|
# (car il a validé antérieurement le dernier semestre)
|
|
# alors on génère aussi un PV de diplome (à la suite dans le même doc PDF)
|
|
if not jury_de_diplome and a_diplome:
|
|
# au moins un etudiant a validé son diplome:
|
|
objects.append(PageBreak())
|
|
objects += _pvjury_pdf_type(
|
|
formsemestre,
|
|
etudids,
|
|
only_diplome=True,
|
|
date_commission=date_commission,
|
|
date_jury=date_jury,
|
|
numero_arrete=numero_arrete,
|
|
code_vdi=code_vdi,
|
|
show_title=show_title,
|
|
pv_title=pv_title,
|
|
with_paragraph_nom=with_paragraph_nom,
|
|
anonymous=anonymous,
|
|
)[0]
|
|
|
|
# ----- Build PDF
|
|
report = io.BytesIO() # in-memory document, no disk file
|
|
document = BaseDocTemplate(report)
|
|
document.pagesize = landscape(A4)
|
|
document.addPageTemplates(
|
|
PVTemplate(
|
|
document,
|
|
author=f"{sco_version.SCONAME} {sco_version.SCOVERSION} (E. Viennet)",
|
|
title=SU(f"PV du jury de {formsemestre.titre_num()}"),
|
|
subject="PV jury",
|
|
preferences=sco_preferences.SemPreferences(formsemestre.id),
|
|
)
|
|
)
|
|
|
|
document.build(objects)
|
|
data = report.getvalue()
|
|
return data
|
|
|
|
|
|
def _make_pv_styles(formsemestre: FormSemestre):
|
|
style = reportlab.lib.styles.ParagraphStyle({})
|
|
style.fontSize = 12
|
|
style.fontName = sco_preferences.get_preference("PV_FONTNAME", formsemestre.id)
|
|
style.leading = 18
|
|
style.alignment = TA_JUSTIFY
|
|
|
|
indent = 1 * cm
|
|
style_bullet = reportlab.lib.styles.ParagraphStyle({})
|
|
style_bullet.fontSize = 12
|
|
style_bullet.fontName = sco_preferences.get_preference(
|
|
"PV_FONTNAME", formsemestre.id
|
|
)
|
|
style_bullet.leading = 12
|
|
style_bullet.alignment = TA_JUSTIFY
|
|
style_bullet.firstLineIndent = 0
|
|
style_bullet.leftIndent = indent
|
|
style_bullet.bulletIndent = indent
|
|
style_bullet.bulletFontName = "Times-Roman"
|
|
style_bullet.bulletFontSize = 11
|
|
style_bullet.spaceBefore = 5 * mm
|
|
style_bullet.spaceAfter = 5 * mm
|
|
return style, style_bullet
|
|
|
|
|
|
def _pvjury_pdf_type(
|
|
formsemestre: FormSemestre,
|
|
etudids: list[int],
|
|
only_diplome=False,
|
|
date_commission=None,
|
|
date_jury=None,
|
|
numero_arrete=None,
|
|
code_vdi=None,
|
|
show_title=False,
|
|
pv_title=None,
|
|
anonymous=False,
|
|
with_paragraph_nom=False,
|
|
) -> tuple[list, bool]:
|
|
"""Objets platypus PDF récapitulant les décisions de jury
|
|
pour un type de jury (passage ou delivrance).
|
|
Ramene: liste d'onj platypus, et un boolen indiquant si au moins un étudiant est diplômé.
|
|
"""
|
|
from app.scodoc import sco_pvjury
|
|
from app.but import jury_but_pv
|
|
|
|
a_diplome = False
|
|
# Jury de diplome si sem. terminal OU que l'on demande seulement les diplomés
|
|
diplome = formsemestre.est_terminal() or only_diplome
|
|
titre_jury, _ = _descr_jury(formsemestre, diplome)
|
|
titre_diplome = pv_title or formsemestre.formation.titre_officiel
|
|
objects = []
|
|
|
|
style, style_bullet = _make_pv_styles(formsemestre)
|
|
|
|
objects += [Spacer(0, 5 * mm)]
|
|
objects += sco_pdf.make_paras(
|
|
f"""
|
|
<para align="center"><b>Procès-verbal de {titre_jury} du département {
|
|
sco_preferences.get_preference("DeptName", formsemestre.id) or "(sans nom)"
|
|
} - Session unique {formsemestre.annee_scolaire()}</b></para>
|
|
""",
|
|
style,
|
|
)
|
|
|
|
objects += sco_pdf.make_paras(
|
|
f"""<para align="center"><b><i>{titre_diplome}</i></b></para>""",
|
|
style,
|
|
)
|
|
|
|
if show_title:
|
|
objects += sco_pdf.make_paras(
|
|
f"""<para align="center"><b>Semestre: {formsemestre.titre}</b></para>""",
|
|
style,
|
|
)
|
|
if sco_preferences.get_preference("PV_TITLE_WITH_VDI", formsemestre.id):
|
|
objects += sco_pdf.make_paras(
|
|
f"""<para align="center">VDI et Code: {(code_vdi or "")}</para>""", style
|
|
)
|
|
|
|
if date_jury:
|
|
objects += sco_pdf.make_paras(
|
|
f"""<para align="center">Jury tenu le {date_jury}</para>""", style
|
|
)
|
|
|
|
objects += sco_pdf.make_paras(
|
|
"<para>"
|
|
+ (sco_preferences.get_preference("PV_INTRO", formsemestre.id) or "")
|
|
% {
|
|
"Decnum": numero_arrete,
|
|
"VDICode": code_vdi,
|
|
"UnivName": sco_preferences.get_preference("UnivName", formsemestre.id),
|
|
"Type": titre_jury,
|
|
"Date": date_commission, # deprecated
|
|
"date_commission": date_commission,
|
|
}
|
|
+ "</para>",
|
|
style_bullet,
|
|
)
|
|
|
|
objects += sco_pdf.make_paras(
|
|
"""<para>Le jury propose les décisions suivantes :</para>""", style
|
|
)
|
|
objects += [Spacer(0, 4 * mm)]
|
|
|
|
if formsemestre.formation.is_apc():
|
|
rows, titles = jury_but_pv.pvjury_table_but(
|
|
formsemestre, etudids=etudids, line_sep="<br/>"
|
|
)
|
|
columns_ids = list(titles.keys())
|
|
a_diplome = codes_cursus.ADM in [row.get("diplome") for row in rows]
|
|
else:
|
|
dpv = sco_dict_pv_jury.dict_pvjury(
|
|
formsemestre.id, etudids=etudids, with_prev=True
|
|
)
|
|
if not dpv:
|
|
return [], False
|
|
rows, titles, columns_ids = sco_pvjury.pvjury_table(
|
|
dpv,
|
|
only_diplome=only_diplome,
|
|
anonymous=anonymous,
|
|
with_paragraph_nom=with_paragraph_nom,
|
|
)
|
|
a_diplome = True in (x["validation_parcours"] for x in dpv["decisions"])
|
|
# convert to lists of tuples:
|
|
columns_ids = ["etudid"] + columns_ids
|
|
rows = [[line.get(x, "") for x in columns_ids] for line in rows]
|
|
titles = [titles.get(x, "") for x in columns_ids]
|
|
# Make a new cell style and put all cells in paragraphs
|
|
cell_style = styles.ParagraphStyle({})
|
|
cell_style.fontSize = sco_preferences.get_preference(
|
|
"SCOLAR_FONT_SIZE", formsemestre.id
|
|
)
|
|
cell_style.fontName = sco_preferences.get_preference("PV_FONTNAME", formsemestre.id)
|
|
cell_style.leading = 1.0 * sco_preferences.get_preference(
|
|
"SCOLAR_FONT_SIZE", formsemestre.id
|
|
) # vertical space
|
|
LINEWIDTH = 0.5
|
|
table_style = [
|
|
(
|
|
"FONTNAME",
|
|
(0, 0),
|
|
(-1, 0),
|
|
sco_preferences.get_preference("PV_FONTNAME", formsemestre.id),
|
|
),
|
|
("LINEBELOW", (0, 0), (-1, 0), LINEWIDTH, Color(0, 0, 0)),
|
|
("GRID", (0, 0), (-1, -1), LINEWIDTH, Color(0, 0, 0)),
|
|
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
|
]
|
|
titles = [f"<para><b>{x}</b></para>" for x in titles]
|
|
|
|
def _format_pv_cell(x):
|
|
"""convert string to paragraph"""
|
|
if isinstance(x, str):
|
|
return Paragraph(SU(x), cell_style)
|
|
else:
|
|
return x
|
|
|
|
widths_by_id = {
|
|
"nom": 5 * cm,
|
|
"cursus": 2.8 * cm,
|
|
"ects": 1.4 * cm,
|
|
"devenir": 1.8 * cm,
|
|
"decision_but": 1.8 * cm,
|
|
}
|
|
|
|
table_cells = [[_format_pv_cell(x) for x in line[1:]] for line in ([titles] + rows)]
|
|
widths = [widths_by_id.get(col_id) for col_id in columns_ids[1:]]
|
|
|
|
objects.append(
|
|
Table(table_cells, repeatRows=1, colWidths=widths, style=table_style)
|
|
)
|
|
|
|
# Signature du directeur
|
|
objects += sco_pdf.make_paras(
|
|
f"""<para spaceBefore="10mm" align="right">{
|
|
sco_preferences.get_preference("DirectorName", formsemestre.id) or ""
|
|
}, {
|
|
sco_preferences.get_preference("DirectorTitle", formsemestre.id) or ""
|
|
}</para>""",
|
|
style,
|
|
)
|
|
|
|
# Légende des codes
|
|
codes = list(codes_cursus.CODES_EXPL.keys())
|
|
codes.sort()
|
|
objects += sco_pdf.make_paras(
|
|
"""<para spaceBefore="15mm" fontSize="14">
|
|
<b>Codes utilisés :</b></para>""",
|
|
style,
|
|
)
|
|
L = []
|
|
for code in codes:
|
|
L.append((code, codes_cursus.CODES_EXPL[code]))
|
|
TableStyle2 = [
|
|
(
|
|
"FONTNAME",
|
|
(0, 0),
|
|
(-1, 0),
|
|
sco_preferences.get_preference("PV_FONTNAME", formsemestre.id),
|
|
),
|
|
("LINEBELOW", (0, 0), (-1, -1), LINEWIDTH, Color(0, 0, 0)),
|
|
("LINEABOVE", (0, 0), (-1, -1), LINEWIDTH, Color(0, 0, 0)),
|
|
("LINEBEFORE", (0, 0), (0, -1), LINEWIDTH, Color(0, 0, 0)),
|
|
("LINEAFTER", (-1, 0), (-1, -1), LINEWIDTH, Color(0, 0, 0)),
|
|
]
|
|
objects.append(
|
|
Table(
|
|
[[Paragraph(SU(x), cell_style) for x in line] for line in L],
|
|
colWidths=(2 * cm, None),
|
|
style=TableStyle2,
|
|
)
|
|
)
|
|
|
|
return objects, a_diplome
|