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 = 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 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 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 # 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) plan_list = self.add_gymnast_planned_skill(gymnast) self.add_gymnast_last_learned_skill(gymnast) self.add_gymnast_next_events(gymnast) self.add_gymnast_week_notes(gymnast) if plan_list: self.new_page() self.add_planned_skills_details(plan_list) def add_gymnast_personnal_information(self, gymnast): """ Ajoute les informations personnelles du gymnast. Args: 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 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 : 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 : 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 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_planned_skill(self, gymnast): """ Ajoute les prochains skill (skill planifié) à apprendre Args: 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 ?) # print(gymnast) # planned_skills = ( # Skill.objects.filter(plan__gymnast=gymnast.id) # # .annotate(plan_date=F("plan__date")) # .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] # ) plan_list = ( Plan.objects.filter(gymnast=gymnast, educative__in=(Skill.objects.all())) .filter( Q(is_done=False) | Q(date__gte=date.today()) ) .order_by('educative', '-date').distinct()[:6] ) if plan_list: for plan in plan_list: print(plan) # self.add_new_line( # X, plan.educative.short_label + " " + str(LEARNING_STEP_CHOICES[plan.learning_step][1]).lower() + " (" + plan.educative.notation + ") for " + plan.date.strftime("%d-%m-%Y") # ) skill = Skill.objects.get(pk=plan.educative) self.add_new_line( X, plan.educative.short_label + " " + str(LEARNING_STEP_CHOICES[plan.learning_step][1]).lower() + " (" + skill.notation + ") for " + plan.date.strftime("%d-%m-%Y") ) else: self.add_new_line( X, "No next skill to learn plannified.", ) print() print() return plan_list 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 += '
' + 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.", ) def add_planned_skills_details(self, plan_list): """ """ # self.y = 20*cm self.add_new_line( X, "Points of attention", font_decoration="Bold", ) self.add_vspace(-0.5*cm) for plan in plan_list: if plan.informations: print(plan) planned_skill = Skill.objects.get(pk=plan.educative) print(planned_skill) # Titre du skill html_text = "" + planned_skill.short_label + " (" + planned_skill.notation + ") :" 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) # Informations du skill pour le gymnaste paragraph = Paragraph(plan.informations, self.style) width, height = paragraph.wrap(18*cm, 10*cm) paragraph.drawOn(self.document, INDENTED_X, self.y) self.add_vspace(- height) self.add_vspace(-0.4*cm)