Bulleins BUT court: html et pdf raccordés

This commit is contained in:
Emmanuel Viennet 2023-09-01 18:14:22 +02:00
parent 265a9119cb
commit 8489d6d108
13 changed files with 230 additions and 65 deletions

View File

@ -44,9 +44,13 @@ from app.views import notes_bp as bp
from app.views import ScoData
@bp.route("/bulletin_but/<int:formsemestre_id>/<int:etudid>")
@bp.route(
"/bulletin_but/<int:formsemestre_id>/<int:etudid>/pdf", defaults={"fmt": "pdf"}
"/bulletin_but/<int:formsemestre_id>/<int:etudid>", endpoint="bulletin_but_html"
)
@bp.route(
"/bulletin_but/<int:formsemestre_id>/<int:etudid>/pdf",
defaults={"fmt": "pdf"},
endpoint="bulletin_but_pdf",
)
@scodoc
@permission_required(Permission.ScoView)

View File

@ -20,12 +20,15 @@ from reportlab.lib.units import cm, mm
from reportlab.platypus import Paragraph, Spacer, Table
from app.but import cursus_but
from app.models import FormSemestre, Identite, ScolarFormSemestreValidation
from app.models import (
BulAppreciations,
FormSemestre,
Identite,
ScolarFormSemestreValidation,
)
from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
from app.scodoc import sco_bulletins
from app.scodoc.sco_logos import Logo
from app.scodoc import sco_pdf, sco_preferences
from app.scodoc.sco_pdf import PDFLOCK, SU
@ -41,6 +44,7 @@ def make_bulletin_but_court_pdf(
ue_validation_by_niveau: dict[tuple[int, str], ScolarFormSemestreValidation] = None,
ues_acronyms: list[str] = None,
) -> bytes:
"génère le bulletin court BUT en pdf"
# A priori ce verrou n'est plus nécessaire avec Flask (multi-process)
# mais...
try:
@ -64,6 +68,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
multi_pages = False # une page par bulletin
small_fontsize = "8"
color_blue_bg = Color(0, 153 / 255, 204 / 255)
color_gray_bg = Color(0.86, 0.86, 0.86)
def __init__(
self,
@ -94,13 +99,23 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
self.nb_ues = len(self.ues_acronyms)
# Styles PDF
self.style_cell = styles.ParagraphStyle("style_cell")
self.style_cell.fontName = "Helvetica"
self.style_base = styles.ParagraphStyle("style_base")
self.style_base.fontName = "Helvetica"
self.style_base.fontSize = 9
self.style_nom = styles.ParagraphStyle("style_nom", self.style_base)
self.style_nom.fontSize = 11
self.style_nom.fontName = "Helvetica-Bold"
self.style_field = self.style_base # écrase style defaut buleltins
self.style_cell = styles.ParagraphStyle("style_cell", self.style_base)
self.style_cell.fontSize = 7
self.style_cell.leading = 7
self.style_bold = styles.ParagraphStyle("style_bold", self.style_cell)
self.style_bold.fontName = "Helvetica-Bold"
self.style_head = styles.ParagraphStyle("style_head", self.style_bold)
self.style_cell_bold = styles.ParagraphStyle("style_cell_bold", self.style_cell)
self.style_cell_bold.fontName = "Helvetica-Bold"
self.style_head = styles.ParagraphStyle("style_head", self.style_cell_bold)
self.style_head.fontSize = 9
self.style_niveaux = styles.ParagraphStyle("style_niveaux", self.style_cell)
@ -128,6 +143,25 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
"style_niveaux_code", self.style_niveaux
)
self.style_niveaux_code.borderColor = black
#
self.style_jury = styles.ParagraphStyle("style_jury", self.style_base)
self.style_jury.fontSize = 9
self.style_jury.leading = self.style_jury.fontSize * 1.4 # espace les lignes
self.style_jury.backColor = self.color_gray_bg
self.style_jury.borderColor = black
self.style_jury.borderWidth = 1
self.style_jury.borderPadding = 2
self.style_jury.borderRadius = 2
self.style_appreciations = styles.ParagraphStyle(
"style_appreciations", self.style_base
)
self.style_appreciations.fontSize = 9
self.style_appreciations.leading = (
self.style_jury.fontSize * 1.4
) # espace les lignes
self.style_assiduite = self.style_cell
# Géométrie page
self.width_page_avail = 185 * mm # largeur utilisable
@ -138,7 +172,27 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
self.width_col_code = self.width_col_ue
# Niveaux
self.width_col_niveaux_titre = 24 * mm
self.width_col_niveaux_code = 12 * mm
self.width_col_niveaux_code = 14 * mm
def bul_title_pdf(self, preference_field="bul_but_pdf_title") -> list:
"""Génère la partie "titre" du bulletin de notes.
Renvoie une liste d'objets platypus
"""
# comme les bulletins standard, mais avec notre préférence
return super().bul_title_pdf(preference_field=preference_field)
def bul_part_below(self, fmt="pdf") -> list:
"""Génère les informations placées sous la table
Dans le cas du bul. court BUT pdf, seulement les appréciations.
fmt est ignoré ici.
"""
appreciations = BulAppreciations.get_appreciations_list(
self.formsemestre.id, self.etud.id
)
return [
Spacer(1, 3 * mm),
self.bul_appreciations_pdf(appreciations, style=self.style_appreciations),
]
def bul_table(self, fmt=None) -> list:
"""Génère la table centrale du bulletin de notes
@ -148,7 +202,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
style_table_2cols = [
("ALIGN", (0, -1), (0, -1), "LEFT"),
("ALIGN", (-1, -1), (-1, -1), "RIGHT"),
("VALIGN", (0, 0), (-1, 1), "TOP"),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("LEFTPADDING", (0, 0), (-1, -1), 0),
("TOPPADDING", (0, 0), (-1, -1), 0),
("RIGHTPADDING", (0, 0), (-1, -1), 0),
@ -156,15 +210,24 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
]
# Ligne avec boite assiduité et table UEs
table_abs_ues = Table(
[[self.box_assiduite(), self.table_ues()]],
colWidths=(3 * cm, self.width_page_avail - 3 * cm),
[
[
self.boite_identite() + [Spacer(1, 3 * mm), self.boite_assiduite()],
self.table_ues(),
],
],
style=style_table_2cols,
)
table_abs_ues.hAlign = "RIGHT"
# Ligne (en bas) avec table cursus et boite jury
table_cursus_jury = Table(
[[self.table_cursus_but(), self.boite_decisions_jury()]],
colWidths=(self.width_page_avail - 45 * mm, 45 * mm),
[
[
self.table_cursus_but(),
[Spacer(1, 8 * mm), self.boite_decisions_jury()],
]
],
colWidths=(self.width_page_avail - 84 * mm, 84 * mm),
style=style_table_2cols,
)
return [
@ -223,13 +286,15 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
col_widths = [self.width_col_ue_titres] + [self.width_col_ue] * self.nb_ues
rows_styled = [[Paragraph(SU(str(cell)), self.style_head) for cell in rows[0]]]
rows_styled += [[Paragraph(SU(str(cell)), self.style_bold) for cell in rows[1]]]
rows_styled += [
[Paragraph(SU(str(cell)), self.style_cell_bold) for cell in rows[1]]
]
rows_styled += [
[Paragraph(SU(str(cell)), self.style_cell) for cell in row]
for row in rows[2:-1]
]
rows_styled += [
[Paragraph(SU(str(cell)), self.style_bold) for cell in rows[-1]]
[Paragraph(SU(str(cell)), self.style_cell_bold) for cell in rows[-1]]
]
table = Table(
rows_styled,
@ -285,7 +350,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
] * self.nb_ues
rows_styled = [
[Paragraph(SU(str(cell)), self.style_bold) for cell in row]
[Paragraph(SU(str(cell)), self.style_cell_bold) for cell in row]
for row in rows[:2]
]
rows_styled += [
@ -310,7 +375,26 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
"saes", "Situations d'Apprentissage et d'Évaluation (SAÉ)"
)
def box_assiduite(self) -> Table:
def boite_identite(self) -> list:
"Les informations sur l'identité et l'inscription de l'étudiant"
return [
Paragraph(
SU(f"""{self.etud.nomprenom}"""),
style=self.style_nom,
),
Paragraph(
SU(
f"""
<b>{self.bul["demission"]}</b><br/>
Formation: {self.formsemestre.titre_num()}<br/>
Année scolaire: {self.formsemestre.annee_scolaire_str()}<br/>
"""
),
style=self.style_base,
),
]
def boite_assiduite(self) -> Table:
"Les informations sur l'assiduité"
if not self.bul["options"]["show_abs"]:
return Paragraph("") # empty
@ -326,7 +410,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
for row in rows[:1]
]
rows_styled += [
[Paragraph(SU(str(cell)), self.style_cell) for cell in row]
[Paragraph(SU(str(cell)), self.style_assiduite) for cell in row]
for row in rows[1:]
]
table = Table(
@ -339,6 +423,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
("SPAN", (0, 1), (1, 1)),
("VALIGN", (0, 0), (-1, -1), "TOP"),
],
colWidths=(25 * mm, 10 * mm),
)
table.hAlign = "LEFT"
return table
@ -387,12 +472,14 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
style=[
("ALIGN", (0, 0), (-1, -1), "CENTER"),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("LEFTPADDING", (0, 0), (-1, -1), 2),
("LEFTPADDING", (0, 0), (-1, -1), 5),
("TOPPADDING", (0, 0), (-1, -1), 4),
("RIGHTPADDING", (0, 0), (-1, -1), 2),
("RIGHTPADDING", (0, 0), (-1, -1), 5),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
# sert de séparateur entre les lignes:
("LINEABOVE", (0, 1), (-1, -1), 3, white),
# séparateur colonne
("LINEBEFORE", (1, 1), (-1, -1), 5, white),
],
)
table.hAlign = "LEFT"
@ -400,24 +487,24 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
def boite_decisions_jury(self):
"""La boite en bas à droite avec jury"""
txt = f"""ECTS acquis : {self.ects_total}<br/>"""
txt = f"""ECTS acquis : {self.ects_total:g}<br/>"""
if self.bul["semestre"]["decision_annee"]:
txt += f"""
Jury tenu le {
datetime.datetime.fromisoformat(self.bul["semestre"]["decision_annee"]["date"]).strftime("%d/%m/%Y à %H:%M")
}, année BUT {self.bul["semestre"]["decision_annee"]["code"]}.
}, année BUT <b>{self.bul["semestre"]["decision_annee"]["code"]}</b>.
<br/>
"""
if self.bul["semestre"]["autorisation_inscription"]:
txt += (
"Autorisé à s'inscrire en "
"Autorisé à s'inscrire en <b>"
+ ", ".join(
[
f"S{aut['semestre_id']}"
for aut in self.bul["semestre"]["autorisation_inscription"]
]
)
+ "."
+ "</b>."
)
return Paragraph(txt)
return Paragraph(txt, style=self.style_jury)

View File

@ -113,10 +113,10 @@ class BulletinGenerator:
self.diagnostic = None # error message if any problem
# Common PDF styles:
# - Pour tous les champs du bulletin sauf les cellules de table:
self.FieldStyle = reportlab.lib.styles.ParagraphStyle({})
self.FieldStyle.fontName = self.preferences["SCOLAR_FONT_BUL_FIELDS"]
self.FieldStyle.fontSize = self.preferences["SCOLAR_FONT_SIZE"]
self.FieldStyle.firstLineIndent = 0
self.style_field = reportlab.lib.styles.ParagraphStyle({})
self.style_field.fontName = self.preferences["SCOLAR_FONT_BUL_FIELDS"]
self.style_field.fontSize = self.preferences["SCOLAR_FONT_SIZE"]
self.style_field.firstLineIndent = 0
# - Pour les cellules de table:
self.CellStyle = reportlab.lib.styles.ParagraphStyle({})
self.CellStyle.fontSize = self.preferences["SCOLAR_FONT_SIZE"]

View File

@ -62,7 +62,7 @@ class BulletinGeneratorLegacy(sco_bulletins_generator.BulletinGenerator):
Renvoie une liste d'objets platypus
"""
objects = sco_bulletins_pdf.process_field(
self.preferences["bul_pdf_title"], self.infos, self.FieldStyle
self.preferences["bul_pdf_title"], self.infos, self.style_field
)
objects.append(
Spacer(1, 5 * mm)
@ -301,7 +301,7 @@ class BulletinGeneratorLegacy(sco_bulletins_generator.BulletinGenerator):
objects += sco_bulletins_pdf.process_field(
self.preferences["bul_pdf_caption"],
self.infos,
self.FieldStyle,
self.style_field,
)
return objects
@ -387,7 +387,7 @@ class BulletinGeneratorLegacy(sco_bulletins_generator.BulletinGenerator):
sco_bulletins_pdf.process_field(
self.preferences["bul_pdf_sig_left"],
self.infos,
self.FieldStyle,
self.style_field,
)
]
]
@ -398,7 +398,7 @@ class BulletinGeneratorLegacy(sco_bulletins_generator.BulletinGenerator):
sco_bulletins_pdf.process_field(
self.preferences["bul_pdf_sig_right"],
self.infos,
self.FieldStyle,
self.style_field,
)
)
else:

View File

@ -142,7 +142,9 @@ class WrapDict(object):
return value
def process_field(field, cdict, style, suppress_empty_pars=False, fmt="pdf"):
def process_field(
field, cdict, style, suppress_empty_pars=False, fmt="pdf", field_name=None
):
"""Process a field given in preferences, returns
- if format = 'pdf': a list of Platypus objects
- if format = 'html' : a string
@ -178,7 +180,7 @@ def process_field(field, cdict, style, suppress_empty_pars=False, fmt="pdf"):
# 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>"
"<para><i>format invalide ! (1)</i></para><para>"
+ traceback.format_exc()
+ "</para>"
)
@ -204,7 +206,9 @@ def process_field(field, cdict, style, suppress_empty_pars=False, fmt="pdf"):
# secure_filename dans la classe Logo
# log('field: %s' % (text))
return sco_pdf.make_paras(text, style, suppress_empty=suppress_empty_pars)
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"):

View File

@ -81,12 +81,15 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
description = "standard ScoDoc (version 2011)"
supported_formats = ["html", "pdf"]
def bul_title_pdf(self) -> list:
def bul_title_pdf(self, preference_field="bul_pdf_title") -> list:
"""Génère la partie "titre" du bulletin de notes.
Renvoie une liste d'objets platypus
"""
objects = sco_bulletins_pdf.process_field(
self.preferences["bul_pdf_title"], self.infos, self.FieldStyle
self.preferences[preference_field],
self.infos,
self.style_field,
field_name=preference_field,
)
objects.append(
Spacer(1, 5 * mm)
@ -195,37 +198,26 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
% self.infos
)
H.append("</div>")
# Appréciations sur PDF:
# ------ Appréciations sur PDF
if appreciations:
story.append(Spacer(1, 3 * mm))
try:
story.append(
Paragraph(
SU(
"Appréciation : "
+ "\n".join(BulAppreciations.summarize(appreciations))
),
self.CellStyle,
)
)
except AttributeError as exc:
raise ScoPDFFormatError(
"Appréciation invalide bloquant la génération du pdf"
) from exc
story.append(self.bul_appreciations_pdf(appreciations))
# ----- DECISION JURY
if self.preferences["bul_show_decision"]:
story += sco_bulletins_pdf.process_field(
self.preferences["bul_pdf_caption"],
self.infos,
self.FieldStyle,
self.style_field,
fmt="pdf",
field_name="bul_pdf_caption",
)
field = sco_bulletins_pdf.process_field(
self.preferences["bul_pdf_caption"],
self.infos,
self.FieldStyle,
self.style_field,
fmt="html",
field_name="bul_pdf_caption",
)
H.append('<div class="bul_decision">' + field + "</div>")
@ -240,6 +232,24 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
elif fmt == "html":
return "\n".join(H)
def bul_appreciations_pdf(
self, appreciations: list[BulAppreciations], style=None
) -> Paragraph:
"Liste d'objets platypus pour les appréciations sous le bulletin"
style = style or self.CellStyle
try:
return Paragraph(
SU(
"Appréciation du "
+ "\n".join(BulAppreciations.summarize(appreciations))
),
style,
)
except AttributeError as exc:
raise ScoPDFFormatError(
"Appréciation invalide bloquant la génération du pdf"
) from exc
def bul_signatures_pdf(self):
"""Génère les signatures placées en bas du bulletin PDF
Renvoie une liste d'objets platypus
@ -253,7 +263,8 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
sco_bulletins_pdf.process_field(
self.preferences["bul_pdf_sig_left"],
self.infos,
self.FieldStyle,
self.style_field,
field_name="bul_pdf_sig_left",
)
]
]
@ -264,7 +275,8 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
sco_bulletins_pdf.process_field(
self.preferences["bul_pdf_sig_right"],
self.infos,
self.FieldStyle,
self.style_field,
field_name="bul_pdf_sig_right",
)
)
else:

View File

@ -45,7 +45,7 @@ CONFIG.LOGO_HEADER_HEIGHT = 28
# server_url: URL du serveur ScoDoc
# scodoc_name: le nom du logiciel (ScoDoc actuellement, voir sco_version.py)
CONFIG.DEFAULT_PDF_FOOTER_TEMPLATE = (
"Edité par %(scodoc_name)s le %(day)s/%(month)s/%(year)s à %(hour)sh%(minute)s"
"Édité par %(scodoc_name)s le %(day)s/%(month)s/%(year)s à %(hour)sh%(minute)s"
)

View File

@ -57,6 +57,7 @@ from flask import g
from app import log
from app.scodoc.sco_exceptions import ScoGenError, ScoPDFFormatError, ScoValueError
from app.scodoc import sco_preferences
from app.scodoc import sco_utils as scu
from app.scodoc.sco_utils import CONFIG
import sco_version
@ -119,7 +120,9 @@ def _splitPara(txt):
return L
def make_paras(txt: str, style, suppress_empty=False) -> list[Paragraph]:
def make_paras(
txt: str, style, suppress_empty=False, field_name=None
) -> list[Paragraph]:
"""Returns a list of Paragraph instances from a text
with one or more <para> ... </para>
"""
@ -157,9 +160,17 @@ def make_paras(txt: str, style, suppress_empty=False) -> list[Paragraph]:
log(traceback.format_exc())
log(f"Invalid pdf para format: {txt}")
try:
# récupère le nom de la préférence
if field_name:
p = sco_preferences.BasePreferences(g.scodoc_dept_id)
pref = p.prefs_dict.get(field_name)
if pref:
field_name = pref["title"]
result = [
Paragraph(
SU('<font color="red"><i>Erreur: format invalide</i></font>'),
SU(
f"""<font color="red"><i>Erreur: format invalide (voir préférence {field_name or ""})</i></font>"""
),
style,
)
]

View File

@ -229,6 +229,16 @@ PREF_CATEGORIES = (
"related": ("abs", "bul_margins", "bul_mail"),
},
),
(
"bul_but_pdf",
{
"title": "Réglages des bulletins BUT (pdf)",
"related": (
"bul",
"bul_margins",
),
},
),
# sur page "Mise en page des bulletins"
(
"bul_margins",
@ -257,7 +267,7 @@ PREF_CATEGORIES = (
)
class BasePreferences(object):
class BasePreferences:
"""Global preferences"""
_editor = ndb.EditableTable(
@ -1681,6 +1691,30 @@ class BasePreferences(object):
"category": "bul",
},
),
# Bulletin court BUT
# avec peu de réglages afin conserver la mise en page compacte...
(
"bul_but_pdf_title",
{
"initvalue": """
<para fontSize="14" align="center">
<b>%(UnivName)s</b>
</para>
<para fontSize="14" align="center" spaceBefore="2mm">
<b>%(InstituteName)s</b>
</para>
<para fontSize="14" align="center" spaceBefore="4mm">
<b>Bachelor Universitaire de Technologie</b>
</para>
""",
"title": "Bulletins PDF BUT: paragraphe de titre",
"explanation": "(balises interprétées, voir documentation)",
"input_type": "textarea",
"rows": 10,
"cols": 64,
"category": "bul_but_pdf",
},
),
# XXX A COMPLETER, voir sco_formsemestre_edit.py XXX
# bul_mail
(

View File

@ -42,6 +42,13 @@
format='pdf',
version=version,
)}}">{{scu.ICON_PDF|safe}}</a>
<a style="margin-left: 20px;"
href="{{url_for(
'notes.bulletin_but_html',
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
etudid=etud.id
)}}">version courte spéciale BUT</a>
</span>
</div>
</form>

View File

@ -39,6 +39,11 @@
{%- endmacro %}
{% block app_content %}
<p><a href="{{url_for(
'notes.bulletin_but_pdf', scodoc_dept=g.scodoc_dept, etudid=etud.id,
formsemestre_id=formsemestre.id
)}}" class="stdlink">version pdf {{scu.ICON_PDF|safe}}</a>
</p>
<div class="but_bul_court">
<div id="infos_etudiant">

View File

@ -11,6 +11,7 @@ SCONEWS = """
<li>ScoDoc 9.6 (juillet 2023)</li>
<ul>
<li>Nouveaux bulletins BUT compacts</li>
<li>Nouvelle gestion des absences et assiduité</li>
<li>Mise à jour logiciels: Debian 12, Python 3.11, ...</li>
</ul>

View File

@ -64,7 +64,7 @@ CONFIG.LOGO_HEADER_HEIGHT = 28 # taille verticale dans le document en millimetr
# scodoc_name: le nom du logiciel (ScoDoc actuellement, voir sco_version.py)
CONFIG.DEFAULT_PDF_FOOTER_TEMPLATE = (
"Edité par %(scodoc_name)s le %(day)s/%(month)s/%(year)s à %(hour)sh%(minute)s"
"Édité par %(scodoc_name)s le %(day)s/%(month)s/%(year)s à %(hour)sh%(minute)s"
)