681 lines
22 KiB
Python
681 lines
22 KiB
Python
import locale
|
|
import os
|
|
import re
|
|
from datetime import date, datetime, timedelta
|
|
from statistics import mean
|
|
|
|
import pendulum
|
|
import yaml
|
|
from django.conf import settings
|
|
from django.db.models import F, Max, Q
|
|
|
|
from PIL import Image
|
|
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 (
|
|
Plan,
|
|
Point,
|
|
Chrono,
|
|
Accident,
|
|
MindState,
|
|
HeightWeight,
|
|
LearnedSkill,
|
|
)
|
|
from ultron.followup.models import LEARNING_STEP_CHOICES
|
|
from ultron.objective.models import Skill
|
|
from ultron.people.models import Gymnast
|
|
from ultron.planning.models import Event
|
|
|
|
from .date_week_transition import from_date_to_week_number
|
|
|
|
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
|
|
INDENT = 5
|
|
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
|
|
self.styles = getSampleStyleSheet()
|
|
self.style = self.styles["Normal"]
|
|
|
|
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)
|
|
self.coach_email = env("HEAD_COACH_EMAIL", default=None)
|
|
|
|
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
|
|
# 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)
|
|
self.add_vspace(15)
|
|
self.add_new_line(X, self.site_title + ' - ' + self.club_name)
|
|
self.document.drawRightString(
|
|
RIGHT_X,
|
|
self.y,
|
|
self.address
|
|
+ " - "
|
|
+ self.zip
|
|
+ " "
|
|
+ self.city,
|
|
)
|
|
|
|
self.add_new_line(
|
|
X,
|
|
"Head Coach : "
|
|
+ self.head_coach
|
|
)
|
|
self.document.drawRightString(
|
|
RIGHT_X,
|
|
self.y,
|
|
self.coach_email
|
|
)
|
|
|
|
today = pendulum.now().date()
|
|
self.add_new_line(X, today.strftime("%d %B %Y"))
|
|
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))
|
|
|
|
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)
|
|
self.add_gymnast_next_skills(gymnast)
|
|
self.add_gymnast_last_learned_skill(gymnast)
|
|
self.add_gymnast_next_events(gymnast)
|
|
self.add_gymnast_week_notes(gymnast)
|
|
|
|
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,
|
|
)
|
|
|
|
self.document.setFillColorRGB(0.75, 0.75, 0.75)
|
|
self.add_new_line(
|
|
130,
|
|
str(gymnast.age)
|
|
)
|
|
self.document.setFillColorRGB(0, 0, 0)
|
|
|
|
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:
|
|
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])
|
|
|
|
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])
|
|
|
|
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
|
|
|
|
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
|
|
|
|
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)
|
|
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",
|
|
)
|
|
|
|
has_score = False
|
|
data = []
|
|
|
|
best_tof = (
|
|
Chrono.objects.filter(gymnast=gymnast)
|
|
.filter(chrono_type=0)
|
|
.order_by("-score")
|
|
.first()
|
|
)
|
|
if best_tof:
|
|
data.append(["ToF |:", str(best_tof.tof), str(best_tof.score), "(" + best_tof.date.strftime("%d-%m-%Y") + ")"])
|
|
has_score = True
|
|
|
|
best_tof = (
|
|
Chrono.objects.filter(gymnast=gymnast)
|
|
.filter(chrono_type=1)
|
|
.order_by("-score")
|
|
.first()
|
|
)
|
|
if best_tof:
|
|
data.append(["ToF R1:", str(best_tof.tof), str(best_tof.score), "(" + best_tof.date.strftime("%d-%m-%Y") + ")"])
|
|
has_score = True
|
|
|
|
best_tof = (
|
|
Chrono.objects.filter(gymnast=gymnast)
|
|
.filter(chrono_type=2)
|
|
.order_by("-score")
|
|
.first()
|
|
)
|
|
if best_tof:
|
|
data.append(["ToF R2:", str(best_tof.tof), str(best_tof.score), "(" + best_tof.date.strftime("%d-%m-%Y") + ")"])
|
|
has_score = True
|
|
|
|
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(
|
|
X,
|
|
"No chrono for this gymnast.",
|
|
)
|
|
|
|
self.y = 20*cm
|
|
self.add_new_line(
|
|
X,
|
|
"Best Scores",
|
|
font_decoration="Bold",
|
|
)
|
|
|
|
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"),
|
|
]
|
|
)
|
|
has_score = True
|
|
|
|
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"),
|
|
]
|
|
)
|
|
has_score = True
|
|
else:
|
|
data.append(
|
|
[
|
|
"R2:",
|
|
"-",
|
|
"-",
|
|
"-",
|
|
"-",
|
|
"-",
|
|
]
|
|
)
|
|
|
|
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(
|
|
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",
|
|
)
|
|
|
|
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()
|
|
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)
|
|
else:
|
|
self.add_new_line(
|
|
14*cm,
|
|
"No compulsary"
|
|
)
|
|
self.add_new_line(
|
|
14*cm,
|
|
"routine defined."
|
|
)
|
|
self.add_vspace(-DOUBLE_LINE_HEIGHT)
|
|
|
|
|
|
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()
|
|
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)
|
|
else:
|
|
self.add_new_line(
|
|
17*cm,
|
|
"No volontary"
|
|
)
|
|
self.add_new_line(
|
|
17*cm,
|
|
"routine defined."
|
|
)
|
|
|
|
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(
|
|
7.5*cm,
|
|
"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(
|
|
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")
|
|
)
|
|
else:
|
|
self.add_new_line(
|
|
7.5*cm,
|
|
"No skill to learn this week.",
|
|
)
|
|
|
|
|
|
def add_gymnast_next_skills(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 ?)
|
|
#
|
|
planified_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"))
|
|
.annotate(plan_date=F("plan__date"), learning_step=F("plan__learning_step"))
|
|
.order_by("notation", "-plan__date").distinct('notation')[:6]
|
|
)
|
|
|
|
if planified_skills:
|
|
for planified_skill in planified_skills:
|
|
self.add_new_line(
|
|
X, planified_skill.short_label + " " + str(LEARNING_STEP_CHOICES[planified_skill.learning_step][1]).lower() + " (" + planified_skill.notation + ") for " + planified_skill.plan_date.strftime("%d-%m-%Y")
|
|
)
|
|
else:
|
|
self.add_new_line(
|
|
X,
|
|
"No next skill to learn plannified.",
|
|
)
|
|
|
|
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])
|
|
|
|
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.",
|
|
)
|
|
|
|
|
|
def add_gymnast_week_notes(self, gymnast):
|
|
""" Ajoute les notes de la semaine du gymnaste passé en paramètre """
|
|
|
|
self.y = 8.8*cm
|
|
self.add_new_line(
|
|
X,
|
|
"Notes",
|
|
font_decoration="Bold",
|
|
)
|
|
self.add_vspace(-2*cm)
|
|
|
|
today = pendulum.today().date()
|
|
begin_of_the_week = today
|
|
if today.weekday() != 0:
|
|
begin_of_the_week -= timedelta(today.weekday())
|
|
|
|
notes = gymnast.remarks.filter(created_at__gte=begin_of_the_week).filter(status=1)
|
|
|
|
if notes:
|
|
html_text = ''
|
|
for note in notes:
|
|
html_text += '<br />' + note.to_markdown()
|
|
|
|
html_text = html_text[6:]
|
|
# print(html_text)
|
|
|
|
paragraph = Paragraph(html_text, self.style)
|
|
width, height = paragraph.wrap(18*cm, 10*cm)
|
|
paragraph.drawOn(self.document, X, self.y - (height / 1.5))
|
|
else:
|
|
self.add_vspace(1.8*cm)
|
|
self.add_new_line(
|
|
X,
|
|
"No note associated to this gymnast this week.",
|
|
)
|