Ultron/ultron/tools/pdf_generator.py

720 lines
23 KiB
Python
Raw Normal View History

import locale
2022-09-27 08:24:01 +02:00
import os
import re
from datetime import date, datetime, timedelta
from statistics import mean
2022-09-27 08:24:01 +02:00
import pendulum
import yaml
from django.conf import settings
from django.db.models import F, Max, Q
2022-09-28 11:47:47 +02:00
from PIL import Image
2022-09-27 08:24:01 +02:00
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.units import cm
from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import Paragraph, Table, TableStyle
from ultron.followup.models import (
2022-10-14 10:35:40 +02:00
Plan,
Point,
Chrono,
2022-10-14 10:35:40 +02:00
Accident,
MindState,
2022-10-14 10:35:40 +02:00
HeightWeight,
LearnedSkill,
)
from ultron.followup.models import LEARNING_STEP_CHOICES
2022-09-27 08:24:01 +02:00
from ultron.objective.models import Skill
from ultron.people.models import Gymnast
2022-09-27 08:24:01 +02:00
from ultron.planning.models import Event
2022-10-06 13:44:49 +02:00
from .date_week_transition import from_date_to_week_number
2022-09-30 12:05:57 +02:00
import environ
from pathlib import Path
# Initialise environment variables
env = environ.Env()
environ.Env.read_env()
# EXPENSES = 0
# RECETTES = 1
X = 35
Y = 841.89
2022-10-16 18:23:36 +02:00
INDENT = 15
RIGHT_X = 595.27 - X
TITLED_X = 125
INDENTED_X = X + INDENT
INDENTED_RIGHT_X = RIGHT_X - INDENT
MIDDLE = (RIGHT_X - X) / 2
PRESTATION_COLUMN_2 = INDENTED_X + 65
PRESTATION_COLUMN_3 = INDENTED_X + 400
PRESTATION_COLUMN_4 = INDENTED_X + 455
COMMON_LINE_HEIGHT = -15
SMALL_LINE_HEIGHT = COMMON_LINE_HEIGHT + 5
LARGE_LINE_HEIGHT = COMMON_LINE_HEIGHT - 5
DOUBLE_LINE_HEIGHT = COMMON_LINE_HEIGHT * 2
BIG_LINE_HEIGHT = COMMON_LINE_HEIGHT * 3
HUGE_LINE_HEIGHT = COMMON_LINE_HEIGHT * 4
class PDFDocument(object):
# Create the PDF object, using the response object as its "file."
# http://www.reportlab.com/docs/reportlab-userguide.pdf
# canvas.rect(x, y, width, height, stroke=1, fill=0)
# localhost:8000/billing/contract/pdf/2
def __init__(self, response):
# Create the PDF object, using the response object as its "file."
self.document = Canvas(response, pagesize=A4)
self.y = Y - X
2022-09-27 08:24:01 +02:00
self.styles = getSampleStyleSheet()
self.style = self.styles["Normal"]
2022-09-30 12:05:57 +02:00
self.site_title = env("SITE_TITLE", default=None)
self.club_name = env("CLUB_NAME", default=None)
self.address = env("ADDRESS", default=None)
self.city = env("CITY", default=None)
self.zip = env("ZIP", default=None)
self.head_coach = env("HEAD_COACH", default=None)
self.mobile_phone = env("MOBILE_PHONE", default=None)
2022-10-01 07:10:25 +02:00
self.coach_email = env("HEAD_COACH_EMAIL", default=None)
2022-10-16 18:23:36 +02:00
def new_page(self):
""" """
# self.y = Y - X
self.document.showPage()
self.y = Y - X
def add_vspace(self, height=COMMON_LINE_HEIGHT):
""" Passe à la ligne, la hauteur de la ligne étant passée en paramètre.
Args:
height (int): hauteur de la ligne.
Returns:
ne retourne rien.
"""
self.y += height
2022-09-30 11:36:03 +02:00
# print(self.y)
# if y < 120;
# document.PageBreak()
# y = 790
def add_string(
self, x, string, font_family="Helvetica", font_decoration=None, font_size=10
):
if font_decoration:
font_family += "-" + font_decoration
self.document.setFont(font_family, font_size)
self.document.drawString(x, self.y, string)
def add_new_line(
self,
x,
string,
height=COMMON_LINE_HEIGHT,
font_family="Helvetica",
font_decoration=None,
font_size=10,
):
self.add_vspace(height)
self.add_string(x, string, font_family, font_decoration, font_size)
def add_header(self, contract=None):
""" Génère le header du document.
Args:
contract (contract): instance de la class Contract.
Returns:
ne retourne rien.
"""
self.document.setFillColorRGB(0.75,0.75,0.75)
2022-09-27 08:24:01 +02:00
self.add_vspace(15)
2022-09-30 12:05:57 +02:00
self.add_new_line(X, self.site_title + ' - ' + self.club_name)
self.document.drawRightString(
RIGHT_X,
self.y,
2022-09-30 12:05:57 +02:00
self.address
+ " - "
2022-09-30 12:05:57 +02:00
+ self.zip
+ " "
2022-09-30 12:05:57 +02:00
+ self.city,
)
2022-09-27 08:24:01 +02:00
self.add_new_line(
X,
"Head Coach : "
2022-09-30 12:05:57 +02:00
+ self.head_coach
)
self.document.drawRightString(
RIGHT_X,
self.y,
2022-10-01 07:10:25 +02:00
self.coach_email
)
2022-09-27 08:24:01 +02:00
today = pendulum.now().date()
2022-10-01 07:10:25 +02:00
self.add_new_line(X, today.strftime("%d %B %Y"))
2022-09-27 08:24:01 +02:00
begin_season = date(today.year, 9, 1)
season, week_number = from_date_to_week_number()
self.document.drawRightString(RIGHT_X, self.y, "Season " + season + " - week " + str(week_number))
2022-09-27 08:24:01 +02:00
self.document.setFillColorRGB(0,0,0)
self.add_vspace(BIG_LINE_HEIGHT)
def download(self):
# Close the PDF object cleanly, and we're done.
self.document.showPage()
self.document.save()
class GymnastReportDocument(PDFDocument):
def generate(self, gymnast_id):
""" Genère un document aux normes du SPF Finance.
Args:
accounting_year (int): année comptable.
Returns:
ne retourne rien.
"""
gymnast = Gymnast.objects.get(pk=gymnast_id)
self.document.setTitle(gymnast.first_name + ' ' + gymnast.last_name)
self.add_header()
self.add_gymnast_personnal_information(gymnast)
self.add_gymnast_physiological_information(gymnast)
self.add_gymnast_best_scores(gymnast)
self.add_gymnast_active_routine(gymnast)
# self.add_gymnast_level_information(gymnast)
2022-10-16 18:23:36 +02:00
planned_skill = self.add_gymnast_planned_skill(gymnast)
2022-10-14 10:35:40 +02:00
self.add_gymnast_last_learned_skill(gymnast)
2022-09-27 08:24:01 +02:00
self.add_gymnast_next_events(gymnast)
self.add_gymnast_week_notes(gymnast)
2022-10-16 18:23:36 +02:00
if planned_skill:
self.new_page()
self.add_planned_skills_details(planned_skill)
def add_gymnast_personnal_information(self, gymnast):
""" Ajoute les informations personnelles du gymnast.
Args:
gymnast <Gymnast>: gymnaste
Returns:
ne retourne rien.
"""
self.y = 26*cm
url = os.path.join(settings.STATICFILES_DIRS[0], "img/default-avatar.png")
self.document.drawImage(url, X, self.y - 65, width=80, height=80)
self.add_string(
130,
str(gymnast),
font_decoration="Bold",
font_size=14,
)
2022-10-04 11:38:25 +02:00
self.document.setFillColorRGB(0.75, 0.75, 0.75)
self.add_new_line(
130,
str(gymnast.age)
)
self.document.setFillColorRGB(0, 0, 0)
2022-10-05 10:38:23 +02:00
if gymnast.informations:
self.add_new_line(
130,
str(gymnast.informations)
)
def analyse_score(self, value, value_list):
""" Analyse une value (value) par rapport à la moyenne de value_list et à la dernière
valeur de value_list
Args:
2022-09-30 21:33:16 +02:00
value float valeur
value_list array<float> liste de valeurs
Returns:
string
Examples:
"""
res = ''
mean_value = mean(value_list)
if value > value_list[-1]:
res += '+'
elif value < value_list[-1]:
res += '-'
else:
res += '='
if value > mean_value:
res = '+' + res
elif value < mean_value:
res = '-' + res
else:
res = '=' + res
return res
def add_gymnast_physiological_information(self, gymnast):
""" Ajoute les informations physique et psychologique.
Args:
gymnast <Gymnast>: gymnaste
Returns:
ne retourne rien
"""
self.y = 26*cm
self.add_new_line(
13.5*cm,
"Physics/Mind state",
font_decoration="Bold",
)
data = []
mindstate_queryset = MindState.objects.filter(gymnast=gymnast).order_by('-date')
last_mindstate = mindstate_queryset.first()
lasts_mindstate = list(mindstate_queryset.values_list("score", flat=True)[1:6])
2022-09-30 15:56:58 +02:00
have_physiological = False
if lasts_mindstate:
res = self.analyse_score(last_mindstate.score, lasts_mindstate)
data.append(["Mind state", str(last_mindstate.score), res])
have_physiological = True
height_weight_queryset = HeightWeight.objects.filter(gymnast=gymnast).order_by('-date')
last_height_weigth = height_weight_queryset.first()
lasts_height = list(height_weight_queryset.values_list("height", flat=True)[1:6])
lasts_weight = list(height_weight_queryset.values_list("weight", flat=True)[1:6])
2022-09-30 15:56:58 +02:00
if lasts_height:
res = self.analyse_score(last_height_weigth.height, lasts_height)
data.append(["Height", str(last_height_weigth.height), res])
have_physiological = True
2022-09-30 15:56:58 +02:00
if lasts_weight:
res = self.analyse_score(last_height_weigth.weight, lasts_weight)
data.append(["Weight", str(last_height_weigth.weight), res])
have_physiological = True
2022-09-30 15:56:58 +02:00
if have_physiological:
style = TableStyle(
[
('ALIGN', (1,0), (-1,-1), 'RIGHT'),
# ('GRID', (0,0), (-1,-1), 0.25, colors.black),
# ('BOX', (0,0), (-1,-1), 0.25, colors.black),
]
)
table = Table(data, [2*cm, 1.5*cm, 1.5*cm])
table.setStyle(style)
width, height = table.wrapOn(self.document, 19*cm, 15.5*cm)
table.drawOn(self.document, 13.3*cm, self.y - height - 5)
2022-09-30 21:33:16 +02:00
else:
self.add_new_line(
13.5*cm,
"No recorded data.",
)
def add_gymnast_best_scores(self, gymnast):
""" Ajoute les meilleurs scores du gymnaste (Tof, compétition, …).
Args:
gymnast <Gymnast>: gymnaste
Returns:
ne retourne rien
"""
self.y = 23*cm
self.add_new_line(
X,
"Best ToF",
font_decoration="Bold",
)
2022-09-30 12:08:49 +02:00
2022-09-30 15:56:58 +02:00
has_score = False
data = []
best_tof = (
Chrono.objects.filter(gymnast=gymnast)
.filter(chrono_type=0)
.order_by("-score")
.first()
)
2022-09-30 12:08:49 +02:00
if best_tof:
data.append(["ToF |:", str(best_tof.tof), str(best_tof.score), "(" + best_tof.date.strftime("%d-%m-%Y") + ")"])
2022-09-30 15:56:58 +02:00
has_score = True
2022-09-30 12:08:49 +02:00
best_tof = (
Chrono.objects.filter(gymnast=gymnast)
.filter(chrono_type=1)
.order_by("-score")
.first()
)
2022-09-30 12:08:49 +02:00
if best_tof:
data.append(["ToF R1:", str(best_tof.tof), str(best_tof.score), "(" + best_tof.date.strftime("%d-%m-%Y") + ")"])
2022-09-30 15:56:58 +02:00
has_score = True
2022-09-30 12:08:49 +02:00
best_tof = (
Chrono.objects.filter(gymnast=gymnast)
.filter(chrono_type=2)
.order_by("-score")
.first()
)
2022-09-30 12:08:49 +02:00
if best_tof:
data.append(["ToF R2:", str(best_tof.tof), str(best_tof.score), "(" + best_tof.date.strftime("%d-%m-%Y") + ")"])
2022-09-30 15:56:58 +02:00
has_score = True
2022-09-30 15:56:58 +02:00
if has_score:
style = TableStyle(
[
('TEXTCOLOR', (-1,0), (-1,-1), '#AAAAAA'),
]
)
table = Table(data)
table.setStyle(style)
width, height = table.wrapOn(self.document, 19*cm, 15*cm)
table.drawOn(self.document, X - 6, self.y - height - 5)
else:
self.add_new_line(
2022-10-04 11:38:25 +02:00
X,
"No chrono for this gymnast.",
)
2022-09-30 15:56:58 +02:00
self.y = 20*cm
self.add_new_line(
X,
"Best Scores",
font_decoration="Bold",
)
2022-09-30 15:56:58 +02:00
has_score = False
data = [["", "Exe.", "Diff.", "ToF", "HD", "Tot.", ""]]
best_point_routine_1 = Point.objects.filter(gymnast=gymnast).filter(routine_type=1).order_by('-total').first()
if best_point_routine_1:
data.append(
[
"R1: ",
best_point_routine_1.point_execution,
best_point_routine_1.point_difficulty,
best_point_routine_1.point_time_of_flight,
best_point_routine_1.point_horizontal_displacement,
best_point_routine_1.total,
best_point_routine_1.event.date_begin.strftime("%d-%m-%Y"),
]
)
2022-09-30 15:56:58 +02:00
has_score = True
2022-09-27 08:24:01 +02:00
best_point_routine_2 = Point.objects.filter(gymnast=gymnast).filter(routine_type=2).order_by('-total').first()
if best_point_routine_2:
data.append(
[
"R2 :",
best_point_routine_2.point_execution,
best_point_routine_2.point_difficulty,
best_point_routine_2.point_time_of_flight,
best_point_routine_2.point_horizontal_displacement,
best_point_routine_2.total,
best_point_routine_2.event.date_begin.strftime("%d-%m-%Y"),
]
)
2022-09-30 15:56:58 +02:00
has_score = True
else:
data.append(
[
"R2:",
"-",
"-",
"-",
"-",
"-",
]
)
2022-09-30 15:56:58 +02:00
if has_score:
style = TableStyle(
[
('ALIGN', (0,0), (-1,0), 'CENTER'),
('ALIGN', (1,1), (-1,-1), 'RIGHT'),
('TEXTCOLOR', (-1,0), (-1,-1), '#AAAAAA'),
# ('BOX', (0, 0), (-1, -1), 0.25, colors.black),
# ('LINEABOVE', (0,-1), (-1,-1), 0.25, colors.black),
]
)
table = Table(data)
table.setStyle(style)
width, height = table.wrapOn(self.document, 19*cm, 15*cm)
table.drawOn(self.document, X - 6, self.y - height - 5)
else:
self.add_new_line(
2022-10-04 11:38:25 +02:00
X,
"No scores for this gymnast.",
)
self.add_vspace(HUGE_LINE_HEIGHT)
def add_gymnast_active_routine(self, gymnast):
""" Ajoute les routines actives """
self.y = 23*cm
self.add_new_line(
15.9*cm,
"Routines",
font_decoration="Bold",
)
2022-09-30 12:12:46 +02:00
routine_1 = gymnast.has_routine.filter(routine_type=1).filter(date_begin__lte=date.today()).filter(Q(date_end__gte=date.today()) | Q(date_end__isnull=True)).first()
2022-09-30 12:12:46 +02:00
if routine_1:
data = []
for routine_skill in routine_1.routine.skill_links.all():
data.append([routine_skill.skill.notation, routine_skill.skill.difficulty])
data.append([None, routine_1.routine.difficulty])
style = TableStyle(
[
('ALIGN', (1,0), (1,-1), 'RIGHT'),
# ('BOX', (0, 0), (-1, -1), 0.25, colors.black),
('LINEABOVE', (0,-1), (-1,-1), 0.25, colors.black),
]
)
table = Table(data, [2*cm, 1*cm])
table.setStyle(style)
width, height = table.wrapOn(self.document, 19*cm, 15*cm)
table.drawOn(self.document, 13.5*cm, self.y - height - 5)
2022-09-30 21:33:16 +02:00
else:
self.add_new_line(
2022-10-04 11:38:25 +02:00
14*cm,
"No compulsary"
)
self.add_new_line(
14*cm,
"routine defined."
)
self.add_vspace(-DOUBLE_LINE_HEIGHT)
2022-09-30 21:33:16 +02:00
routine_2 = gymnast.has_routine.filter(routine_type=2).filter(date_begin__lte=date.today()).filter(Q(date_end__gte=date.today()) | Q(date_end__isnull=True)).first()
2022-09-30 12:12:46 +02:00
if routine_2:
data = []
for routine_skill in routine_2.routine.skill_links.all():
data.append([routine_skill.skill.notation, routine_skill.skill.difficulty])
data.append([None, routine_2.routine.difficulty])
style = TableStyle(
[
('ALIGN', (1,0), (1,-1), 'RIGHT'),
# ('BOX', (0, 0), (-1, -1), 0.25, colors.black),
('LINEABOVE', (0,-1), (-1,-1), 0.25, colors.black),
]
)
table = Table(data, [2*cm, 1*cm])
table.setStyle(style)
width, height = table.wrapOn(self.document, 19*cm, 15.5*cm)
table.drawOn(self.document, 17*cm, self.y - height - 5)
2022-09-30 21:33:16 +02:00
else:
self.add_new_line(
2022-10-04 11:38:25 +02:00
17*cm,
"No volontary"
)
self.add_new_line(
17*cm,
"routine defined."
)
2022-10-14 10:35:40 +02:00
def add_gymnast_last_learned_skill(self, gymnast):
""" Ajoute les derniers skill appris par le gymnaste
Args:
gymnast <Gymnast> gymnaste
Returns:
Ne retourne rien
"""
self.y = 17*cm
self.add_new_line(
2022-10-14 10:46:34 +02:00
7.5*cm,
2022-10-14 10:35:40 +02:00
"New learned skills",
font_decoration="Bold",
)
self.add_vspace(-3)
# le double F ne fonctionne qu'en précisant le distinct, sinon ca dédouble les résultats.
# qui lui même ne fonctionne que sur un champ présent dans le `order_by` (que le premier champ ?)
#
learned_skills = (
LearnedSkill.objects.filter(gymnast=gymnast.id)
.annotate(skill_notation=F("skill__notation"))
.order_by("skill_notation", "-date").distinct('skill_notation')[:5]
)
if learned_skills:
for learned_skill in learned_skills:
self.add_new_line(
2022-10-14 10:46:34 +02:00
7.5*cm, learned_skill.skill.short_label + " " + str(LEARNING_STEP_CHOICES[learned_skill.learning_step][1]).lower() + " (" + learned_skill.skill.notation + "), " + learned_skill.date.strftime("%d-%m-%Y")
2022-10-14 10:35:40 +02:00
)
else:
self.add_new_line(
2022-10-14 10:46:34 +02:00
7.5*cm,
2022-10-14 10:35:40 +02:00
"No skill to learn this week.",
)
2022-10-16 18:23:36 +02:00
def add_gymnast_planned_skill(self, gymnast):
""" Ajoute les prochains skill (skill planifié) à apprendre
Args:
gymnast <Gymnast> gymnaste
Returns:
Ne retourne rien
"""
self.y = 17*cm
self.add_new_line(
X,
"Next skills to learn",
font_decoration="Bold",
)
self.add_vspace(-3)
# le double F ne fonctionne qu'en précisant le distinct, sinon ca dédouble les résultats.
# qui lui même ne fonctionne que sur un champ présent dans le `order_by` (que le premier champ ?)
#
2022-10-16 18:23:36 +02:00
planned_skills = (
Skill.objects.filter(plan__gymnast=gymnast.id)
.filter(
Q(plan__is_done=False)
| Q(plan__date__gte=date.today())
)
# .annotate(plan_date=F("plan__date"))
2022-10-16 18:23:36 +02:00
.annotate(plan_date=F("plan__date"), learning_step=F("plan__learning_step"), plan_id=F("plan__id"))
.order_by("id", "-plan__date").distinct('id')[:6]
)
2022-10-16 18:23:36 +02:00
if planned_skills:
for planned_skill in planned_skills:
2022-09-30 15:56:58 +02:00
self.add_new_line(
2022-10-16 18:23:36 +02:00
X, planned_skill.short_label + " " + str(LEARNING_STEP_CHOICES[planned_skill.learning_step][1]).lower() + " (" + planned_skill.notation + ") for " + planned_skill.plan_date.strftime("%d-%m-%Y")
2022-09-30 15:56:58 +02:00
)
else:
self.add_new_line(
2022-09-30 15:56:58 +02:00
X,
2022-10-12 10:30:46 +02:00
"No next skill to learn plannified.",
2022-09-30 15:56:58 +02:00
)
2022-10-16 18:23:36 +02:00
return planned_skills
2022-09-27 08:24:01 +02:00
def add_gymnast_next_events(self, gymnast):
""" Ajoute les évènements futurs du gymnaste """
self.y = 13.5*cm
self.add_new_line(
X,
"Next event",
font_decoration="Bold",
)
self.add_vspace(-3)
today = pendulum.now().date()
next_event_list = Event.objects.filter(gymnasts=gymnast.id, date_begin__gte=today).order_by("date_begin")[:5]
data = []
for event in next_event_list:
data.append([event.date_begin.strftime("%d-%m-%Y"), "in " + str(event.number_of_week_from_today) + " week(s)", event.name])
2022-09-30 15:56:58 +02:00
if data:
style = TableStyle(
[
('ALIGN', (1,0), (1,-1), 'CENTER'),
# ('BOX', (0, 0), (-1, -1), 0.25, colors.black),
# ('GRID', (0,0), (-1,-1), 0.25, colors.black),
# ('LINEABOVE', (0,-1), (-1,-1), 0.25, colors.black),
]
)
table = Table(data, [2.3*cm, 2.2*cm, 8*cm])
table.setStyle(style)
width, height = table.wrapOn(self.document, 19*cm, 15.5*cm)
table.drawOn(self.document, X - 6, self.y - height - 5)
else:
self.add_new_line(
X,
"No futur event associated to this gymnast.",
)
2022-09-27 08:24:01 +02:00
def add_gymnast_week_notes(self, gymnast):
""" Ajoute les notes de la semaine du gymnaste passé en paramètre """
2022-10-05 10:05:11 +02:00
self.y = 8.8*cm
2022-09-27 08:24:01 +02:00
self.add_new_line(
X,
"Notes",
font_decoration="Bold",
)
self.add_vspace(-2*cm)
today = pendulum.today().date()
2022-09-30 11:36:03 +02:00
begin_of_the_week = today
2022-09-27 08:24:01 +02:00
if today.weekday() != 0:
2022-09-30 11:36:03 +02:00
begin_of_the_week -= timedelta(today.weekday())
2022-09-27 08:24:01 +02:00
2022-10-07 09:31:56 +02:00
notes = gymnast.remarks.filter(created_at__gte=begin_of_the_week).filter(status=1)
2022-09-27 08:24:01 +02:00
2022-09-30 15:56:58 +02:00
if notes:
html_text = ''
for note in notes:
html_text += '<br />' + note.to_markdown()
2022-09-27 08:24:01 +02:00
2022-09-30 15:56:58 +02:00
html_text = html_text[6:]
# print(html_text)
2022-09-27 08:24:01 +02:00
2022-10-05 09:47:43 +02:00
paragraph = Paragraph(html_text, self.style)
width, height = paragraph.wrap(18*cm, 10*cm)
paragraph.drawOn(self.document, X, self.y - (height / 1.5))
2022-09-30 15:56:58 +02:00
else:
2022-09-30 21:33:16 +02:00
self.add_vspace(1.8*cm)
2022-09-30 15:56:58 +02:00
self.add_new_line(
X,
2022-09-30 21:33:16 +02:00
"No note associated to this gymnast this week.",
2022-09-30 15:56:58 +02:00
)
2022-10-16 18:23:36 +02:00
def add_planned_skills_details(self, planned_skills):
""" """
# self.y = 20*cm
self.add_new_line(
X,
"Points of attention",
font_decoration="Bold",
)
self.add_vspace(-0.5*cm)
for planned_skill in planned_skills:
plan = Plan.objects.get(pk=planned_skill.plan_id)
# Titre du skill
2022-10-16 18:23:36 +02:00
html_text = "<u>" + planned_skill.short_label + " (" + planned_skill.notation + ") :</u>"
paragraph = Paragraph(html_text, self.style)
width, height = paragraph.wrap(18*cm, 10*cm)
paragraph.drawOn(self.document, INDENTED_X, self.y)
self.add_vspace(- height - 0.5*cm)
2022-10-16 18:23:36 +02:00
# Informations du skill pour le gymnaste
paragraph = Paragraph(plan.informations, self.style)
2022-10-17 11:07:10 +02:00
width, height = paragraph.wrap(18*cm, 10*cm)
paragraph.drawOn(self.document, INDENTED_X, self.y)
self.add_vspace(- height)
2022-10-17 11:07:10 +02:00
2022-10-16 18:23:36 +02:00
self.add_vspace(-0.4*cm)