from io import BytesIO import extract_msg import nh3 from bs4 import BeautifulSoup from pypdf import PdfWriter from functools import partial from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_JUSTIFY from reportlab.lib.pagesizes import A4 from reportlab.lib import colors from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet from reportlab.lib.units import cm from reportlab.pdfgen import canvas from reportlab.platypus import ( PageBreak, Paragraph, Preformatted, SimpleDocTemplate, Spacer, Table, TableStyle ) from django.contrib.staticfiles.finders import find from django.utils.text import slugify from .utils import format_d_m_Y, format_duree, format_Ymd def format_booleen(val): return '?' if val is None else ('oui' if val else 'non') class PageNumCanvas(canvas.Canvas): """A special canvas to be able to draw the total page number in the footer.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._pages = [] def showPage(self): self._pages.append(dict(self.__dict__)) self._startPage() def save(self): page_count = len(self._pages) for page in self._pages: self.__dict__.update(page) self.draw_page_number(page_count) canvas.Canvas.showPage(self) super().save() def draw_page_number(self, page_count): self.setFont("Helvetica", 9) self.drawRightString(self._pagesize[0] - 1.6*cm, 2.3*cm, "p. %s/%s" % (self._pageNumber, page_count)) class CleanParagraph(Paragraph): """If the (HTML) text cannot be parsed, try to clean it.""" def __init__(self, text, *args, **kwargs): if text: text = text.replace('

', '


') if '','
').replace('', '
') try: super().__init__(text, *args, **kwargs) except ValueError: text = nh3.clean( text, tags={'p', 'br', 'b', 'strong', 'u', 'i', 'em', 'ul', 'li'} ).replace('
', '
') super().__init__(text, *args, **kwargs) class RawParagraph(Paragraph): """Raw text, replace new lines by
.""" def __init__(self, text='', *args, **kwargs): if text: text = text.replace('\r\n', '\n').replace('\n', '\n
') super().__init__(text, *args, **kwargs) class StyleMixin: FONTSIZE = 9 MAXLINELENGTH = 120 def __init__(self, base_font='Helvetica', **kwargs): self.styles = getSampleStyleSheet() self.style_title = ParagraphStyle( name='title', fontName='Helvetica-Bold', fontSize=self.FONTSIZE + 4, leading=self.FONTSIZE + 5, alignment=TA_CENTER ) self.style_normal = ParagraphStyle( name='normal', fontName='Helvetica', fontSize=self.FONTSIZE, alignment=TA_LEFT, leading=self.FONTSIZE + 1, spaceAfter=0 ) self.style_justifie = ParagraphStyle( name='justifie', parent=self.style_normal, alignment=TA_JUSTIFY, spaceAfter=0.2 * cm ) self.style_sub_title = ParagraphStyle( name='sous_titre', fontName='Helvetica-Bold', fontSize=self.FONTSIZE + 2, alignment=TA_LEFT, spaceBefore=0.5 * cm, spaceAfter=0.1 * cm ) self.style_inter_title = ParagraphStyle( name='inter_titre', fontName='Helvetica-Bold', fontSize=self.FONTSIZE + 1, alignment=TA_LEFT, spaceBefore=0.3 * cm, spaceAfter=0 ) self.style_bold = ParagraphStyle( name='bold', fontName='Helvetica-Bold', fontSize=self.FONTSIZE, leading=self.FONTSIZE + 1 ) self.style_italic = ParagraphStyle( name='italic', fontName='Helvetica-Oblique', fontSize=self.FONTSIZE - 1, leading=self.FONTSIZE ) self.style_indent = ParagraphStyle( name='indent', fontName='Helvetica', fontSize=self.FONTSIZE, alignment=TA_LEFT, leftIndent=1 * cm ) super().__init__(**kwargs) class HeaderFooterMixin: LOGO = find('img/logo-cr.png') EDUQUA = find('img/eduqua.png') DON = find('img/logo-zewo.png') def draw_header(self, canvas, doc): canvas.saveState() canvas.drawImage( self.LOGO, doc.leftMargin + 316, doc.height+60, 7 * cm, 1.6 * cm, preserveAspectRatio=True, mask='auto' ) canvas.restoreState() def draw_footer(self, canvas, doc): canvas.saveState() canvas.drawImage( self.EDUQUA, doc.leftMargin, doc.height - 670, 1.8 * cm, 0.8 * cm, preserveAspectRatio=True ) canvas.drawImage( self.DON, doc.leftMargin + 60, doc.height - 670, 2.5 * cm, 0.8 * cm, preserveAspectRatio=True, mask='auto' ) tab = [220, 365] line = [658, 667, 676, 685] canvas.setFont("Helvetica", 8) canvas.drawRightString(doc.leftMargin + tab[0], doc.height - line[2], "CCP ?") canvas.drawRightString(doc.leftMargin + tab[0], doc.height - line[3], "IBAN ?") canvas.setLineWidth(0.5) canvas.line(doc.leftMargin + 230, 2.2 * cm, doc.leftMargin + 230, 1.0 * cm) canvas.drawRightString(doc.leftMargin + tab[1], doc.height - line[0], "Rte d’Englisberg 3") canvas.setFont("Helvetica-Bold", 8) canvas.drawRightString(doc.leftMargin + tab[1], doc.height - line[1], "1763 Granges-Paccot") canvas.setFont("Helvetica", 8) canvas.line(doc.leftMargin + 375, 2.2 * cm, doc.leftMargin + 375, 1.0 * cm) canvas.drawRightString(doc.leftMargin + doc.width, doc.height - line[0], "+41 26 407 70 44") canvas.drawRightString(doc.leftMargin + doc.width, doc.height - line[1], "secretariat@fondation-transit.ch") canvas.drawRightString(doc.leftMargin + doc.width, doc.height - line[2], "fondation-transit.ch/aemo") canvas.restoreState() class BasePDF(HeaderFooterMixin, StyleMixin): def __init__(self, tampon, instance, **kwargs): self.instance = instance self.kwargs = kwargs self.doc = SimpleDocTemplate( tampon, title=self.title, pagesize=A4, leftMargin=1.5 * cm, rightMargin=1.5 * cm, topMargin=2 * cm, bottomMargin=2.5 * cm ) self.story = [] super().__init__(**kwargs) def draw_header_footer(self, canvas, doc): self.draw_header(canvas, doc) self.draw_footer(canvas, doc) def produce(self): # subclass should call self.doc.build(self.story, onFirstPage=self.draw_header_footer) raise NotImplementedError def get_filename(self): raise NotImplementedError @staticmethod def format_note(note, length=StyleMixin.MAXLINELENGTH): return Preformatted(note.replace('\r\n', '\n'), maxLineLength=length) def set_title(self, title=''): self.story.append(Spacer(0, 1 * cm)) self.story.append(Paragraph(title, self.style_title)) self.story.append(Spacer(0, 1.2 * cm)) def parent_data(self, person): """Return parent data ready to be used in a 2-column table.""" parents = person.parents() par1 = parents[0] if len(parents) > 0 else None par2 = parents[1] if len(parents) > 1 else None data = [ [('Parent 1 (%s)' % par1.role.nom) if par1 else '-', ('Parent 2 (%s)' % par2.role.nom) if par2 else '-'], [par1.contact.nom_prenom if par1 else '', par2.contact.nom_prenom if par2 else ''], [par1.contact.adresse if par1 else '', par2.contact.adresse if par2 else ''], [par1.contact.contact if par1 else '', par2.contact.contact if par2 else ''], ['Autorité parentale: {}'.format(format_booleen(par1.contact.autorite_parentale)) if par1 else '', 'Autorité parentale: {}'.format(format_booleen(par2.contact.autorite_parentale)) if par2 else ''], ] return data def formate_persons(self, parents_list): labels = ( ("Nom", "nom"), ("Prénom", "prenom"), ("Adresse", "rue"), ("Localité", 'localite_display'), ("Profession", 'profession'), ("Tél.", 'telephone'), ("Courriel", 'email'), ('Remarque', 'remarque'), ) P = partial(Paragraph, style=self.style_normal) Pbold = partial(Paragraph, style=self.style_bold) data = [] parents = [parent for parent in parents_list if parent is not None] if len(parents) == 0: pass elif len(parents) == 1: data.append([Pbold('Rôle:'), Pbold(parents[0].role.nom), '', '']) for label in labels: data.append([ label[0], P(getattr(parents[0], label[1])), '', '' ]) elif len(parents) == 2: data.append([ Pbold('Rôle:'), Pbold(parents[0].role.nom), Pbold('Rôle:'), Pbold(parents[1].role.nom) ]) for label in labels: data.append([ label[0], P(getattr(parents[0], label[1]) if parents[0] else ''), label[0], P(getattr(parents[1], label[1]) if parents[1] else '') ]) return data def get_table(self, data, columns, before=0.0, after=0.0, inter=None): """Prepare a Table instance with data and columns, with a common style.""" if inter: inter = inter * cm cols = [c * cm for c in columns] t = Table( data=data, colWidths=cols, hAlign=TA_LEFT, spaceBefore=before * cm, spaceAfter=after * cm ) t.hAlign = 0 t.setStyle(tblstyle=TableStyle([ ('ALIGN', (0, 0), (-1, -1), 'LEFT'), ('FONTSIZE', (0, 0), (-1, -1), self.FONTSIZE), ('VALIGN', (0, 0), (-1, -1), "TOP"), ('LEFTPADDING', (0, 0), (0, -1), 1), ('LEADING', (0, 0), (-1, -1), 7), ])) return t def _add_subtitle(self, data): t = Table( data=[data], colWidths=[18 * cm / len(data)] * len(data), hAlign=TA_LEFT, spaceBefore=0.2 * cm, spaceAfter=0.5 * cm ) t.hAlign = 0 t.setStyle(tblstyle=TableStyle([ ('FONT', (0, 0), (-1, -1), "Helvetica-Bold"), ('FONTSIZE', (0, 0), (-1, -1), self.FONTSIZE + 2), ('LINEBELOW', (0, -1), (-1, -1), 0.25, colors.black), ('ALIGN', (-1, -1), (-1, -1), 'RIGHT'), ])) self.story.append(t) def write_paragraph(self, title, text, html=False, justifie=False): if title: self.story.append(Paragraph(title, self.style_sub_title)) style = self.style_justifie if justifie else self.style_normal if html: if text.startswith('

') and text.endswith('

'): soup = BeautifulSoup(text, features="html5lib") for tag in soup.find_all(['p', 'ul']): self.story.append(CleanParagraph(str(tag), style)) else: self.story.append(CleanParagraph(text, style)) else: self.story.append(RawParagraph(text, style)) def enfant_data(self, enfant): labels = [('Tél.', enfant.telephone)] if hasattr(enfant, 'formation'): labels.extend([ ('Statut scol', enfant.formation.get_statut_display()), ('Centre', enfant.formation.cercle_scolaire), ('Collège', enfant.formation.college), ('Classe', enfant.formation.classe), ('Struct. extra-fam.', enfant.formation.creche), ('Ens.', enfant.formation.enseignant), ]) labels.extend([ ('Permis de séjour', enfant.permis), ('Validité', enfant.validite), ]) row = [f"{enfant.nom_prenom} (*{format_d_m_Y(enfant.date_naissance)})"] for label in labels: if label[1]: row.append(f"{label[0]}: {label[1]}") return '; '.join(row) class DemandeAccompagnement(BasePDF): title = None def produce(self): famille = self.instance suivi = famille.suivi self.set_title("Famille {} - {}".format(famille.nom, self.title)) self.story.append(Paragraph("Motif(s) de la demande: {}".format( suivi.get_motif_demande_display()), self.style_normal )) self.story.append(Paragraph('_' * 90, self.style_normal)) self.write_paragraph("Dates", suivi.dates_demande) self.write_paragraph( "Difficultés", "{}
{}

{}".format( "Quelles sont les difficultés éducatives que vous rencontrez et depuis combien de " "temps ?", "Fonctionnement familial: règles, coucher, lever, repas, jeux, relations " "parent-enfants, rapport au sein de la fratrie, … (exemple)", suivi.difficultes ), html=True ) self.write_paragraph( "Autres services", "{}
{}".format( "Avez-vous fait appel à d'autres services ? Si oui, avec quels vécus ?", suivi.autres_contacts ), html=True ) self.write_paragraph("Aides souhaitées", suivi.aides, html=True) self.write_paragraph("Ressources/Compétences", suivi.competences, html=True) self.write_paragraph("Disponibilités", suivi.disponibilites, html=True) self.write_paragraph("Remarques", suivi.remarque) self.doc.build(self.story, onFirstPage=self.draw_header_footer) class JournalPdf(BasePDF): title = "Journal de bord" def get_filename(self): return '{}_journal.pdf'.format(slugify(self.instance.nom)) def get_title(self): return 'Famille {} - {}'.format(self.instance.nom, self.title) def produce(self): famille = self.instance self.set_title(self.get_title()) self.style_bold.spaceAfter = 0.2*cm self.style_italic.spaceBefore = 0.2 * cm self.style_italic.spaceAfter = 0.7 * cm for prest in famille.prestations.all().prefetch_related('intervenants'): self.story.append(CleanParagraph(prest.texte, self.style_normal)) self.story.append( Paragraph('{} - {} ({})'.format( '/'.join(interv.sigle for interv in prest.intervenants.all()), format_d_m_Y(prest.date_prestation), format_duree(prest.duree) ), self.style_italic) ) self.doc.build( self.story, onFirstPage=self.draw_header_footer, onLaterPages=self.draw_footer, canvasmaker=PageNumCanvas ) class RapportPdf(BasePDF): title = None def get_filename(self): return "{}_resume_{}.pdf".format( slugify(self.instance.famille.nom), format_Ymd(self.instance.date) ) def produce(self): rapport = self.instance self.style_normal.fontSize += 2 self.style_normal.leading += 2 self.style_justifie.fontSize += 2 self.style_justifie.leading += 2 self.doc.title = 'Résumé "AEMO"' self.set_title('Famille {} - {}'.format(rapport.famille.nom, self.doc.title)) self._add_subtitle([f"Date: {format_d_m_Y(rapport.date)}"]) data = [ ("Enfant(s)", '
'.join( [f"{enfant.nom_prenom} (*{format_d_m_Y(enfant.date_naissance)})" for enfant in rapport.famille.membres_suivis()]), False), ("Intervenant-e-s", ', '.join([i.nom_prenom for i in rapport.intervenants()]), False), ("Début du suivi", format_d_m_Y(rapport.famille.suivi.date_debut_suivi), False), ("Situation / contexte familial", rapport.situation, True), ] data.append(("Observations", rapport.observations, True)) data.append(("Perspectives d'avenir", rapport.projet, True)) for title, text, html in data: self.write_paragraph(title, text, html=html, justifie=True) if hasattr(rapport, 'sig_interv'): self.story.append(Spacer(0, 0.5 * cm)) for idx, interv in enumerate(rapport.sig_interv.all()): if idx == 0: self.write_paragraph("Signature des intervenant-e-s :", '') self.story.append(Spacer(0, 0.2 * cm)) self.write_paragraph('', interv.nom_prenom + (f', {interv.profession}' if interv.profession else '')) self.story.append(Spacer(0, 1 * cm)) secret_style = ParagraphStyle( name='italic', fontName='Helvetica-Oblique', fontSize=self.FONTSIZE - 1, leading=self.FONTSIZE + 1, backColor=colors.Color(0.96, 0.96, 0.96, 1), borderRadius=12, ) self.story.append(Paragraph( "Le présent résumé comporte des éléments couverts par le secret professionnel au sens " "de la LPSy et du Code pénal. Seuls les propriétaires des données, à savoir les membres " "de la famille faisant l’objet du résumé, peuvent ensemble lever ce secret ou " "accepter la divulgation des données. Si cette autorisation n’est pas donnée, l’autorité " "compétente en matière de levée du secret professionnel doit impérativement être saisie.", secret_style )) self.doc.build(self.story, onFirstPage=self.draw_header_footer, onLaterPages=self.draw_footer) class MessagePdf(BasePDF): title = 'Message' def get_filename(self): return '{}_message.pdf'.format(slugify(self.instance.subject)) def produce(self): doc = self.instance self.set_title('{} - Famille {}'.format(self.title, doc.famille.nom)) with extract_msg.Message(doc.fichier.path) as msg: P = partial(Paragraph, style=self.style_normal) Pbold = partial(Paragraph, style=self.style_bold) msg_headers = [ [Pbold('De:'), P(msg.sender)], [Pbold('À:'), P(msg.to)], ] if msg.cc: msg_headers.append([Pbold('CC:'), P(msg.cc)]) if msg.date: msg_headers.append([Pbold('Date:'), P(msg.date)]) msg_headers.append([Pbold('Sujet:'), P(msg.subject)]) self.story.append(self.get_table(msg_headers, [3, 15])) self.story.append(Pbold('Message:')) self.story.append(RawParagraph(msg.body, style=self.style_normal)) return self.doc.build( self.story, onFirstPage=self.draw_header_footer, onLaterPages=self.draw_footer, canvasmaker=PageNumCanvas ) class EvaluationPdf: def __init__(self, tampon, famille): self.tampon = tampon self.famille = famille self.merger = PdfWriter() def append_pdf(self, PDFClass): tampon = BytesIO() pdf = PDFClass(tampon, self.famille) pdf.produce() self.merger.append(tampon) def produce(self): self.append_pdf(CoordonneesPagePdf) self.append_pdf(DemandeAccompagnementPagePdf) self.merger.write(self.tampon) def get_filename(self): return '{}_aemo_evaluation.pdf'.format(slugify(self.famille.nom)) class CoordonneesFamillePdf(BasePDF): title = "Informations" def get_filename(self): return '{}_coordonnees.pdf'.format(slugify(self.instance.nom)) def produce(self): famille = self.instance suivi = famille.suivi self.set_title('Famille {} - {}'.format(famille.nom, self.title)) # Parents self.story.append(Paragraph('Parents', self.style_sub_title)) data = self.formate_persons(famille.parents()) if data: self.story.append(self.get_table(data, columns=[2, 7, 2, 7], before=0, after=0)) else: self.story.append(Paragraph('-', self.style_normal)) # Situation matrimoniale data = [ ['Situation matrimoniale: {}'.format(famille.get_statut_marital_display()), 'Autorité parentale: {}'.format(famille.get_autorite_parentale_display())], ] self.story.append(self.get_table(data, columns=[9, 9], before=0, after=0)) # Personnes significatives autres_parents = list(famille.autres_parents()) if autres_parents: self.story.append(Paragraph('Personne-s significative-s', self.style_sub_title)) data = self.formate_persons(autres_parents) self.story.append(self.get_table(data, columns=[2, 7, 3, 6], before=0, after=0)) if len(autres_parents) > 2: self.story.append(PageBreak()) # Enfants suivis self.write_paragraph( "Enfant(s)", '
'.join(self.enfant_data(enfant) for enfant in famille.membres_suivis()) ) # Réseau self.story.append(Paragraph("Réseau", self.style_sub_title)) data = [ ['AS OPE', '{} - (Mandat: {})'.format( ', '.join(ope.nom_prenom for ope in suivi.ope_referents), suivi.get_mandat_ope_display() )], ['Interv. CRNE', '{}'.format( ', '.join('{}'.format(i.nom_prenom) for i in suivi.intervenants.all().distinct()) )] ] for enfant in famille.membres_suivis(): for contact in enfant.reseaux.all(): data.append([ enfant.prenom, '{} ({})'.format(contact, contact.contact) ]) self.story.append(self.get_table(data, columns=[2, 16], before=0, after=0)) self.write_paragraph("Motif de la demande", famille.suivi.motif_detail) self.write_paragraph("Collaborations", famille.suivi.collaboration) # Historique self.story.append(Paragraph('Historique', self.style_sub_title)) P = partial(Paragraph, style=self.style_normal) Pbold = partial(Paragraph, style=self.style_bold) fields = ['date_demande', 'date_debut_evaluation', 'date_fin_evaluation', 'date_debut_suivi', 'date_fin_suivi'] data = [] for field_name in fields: field = famille.suivi._meta.get_field(field_name) if getattr(famille.suivi, field_name): data.append( [Pbold(f"{field.verbose_name} :"), P(format_d_m_Y(getattr(famille.suivi, field_name)))] ) if famille.suivi.motif_fin_suivi: data.append([Pbold("Motif de fin de suivi :"), famille.suivi.get_motif_fin_suivi_display()]) if famille.destination: data.append([Pbold("Destination :"), famille.get_destination_display()]) if famille.archived_at: data.append([Pbold("Date d'archivage :"), format_d_m_Y(famille.archived_at)]) self.story.append(self.get_table(data, [4, 5])) self.doc.build(self.story, onFirstPage=self.draw_header_footer) class DemandeAccompagnementPagePdf(DemandeAccompagnement): title = "Évaluation AEMO" class BilanPdf(BasePDF): title = "Bilan AEMO" def get_filename(self): return "{}_bilan_{}.pdf".format( slugify(self.instance.famille.nom), format_Ymd(self.instance.date) ) def produce(self): bilan = self.instance self.style_normal.fontSize += 2 self.style_normal.leading += 2 self.style_justifie.fontSize += 2 self.style_justifie.leading += 2 self.set_title('Famille {} - {}'.format(bilan.famille.nom, self.title)) self._add_subtitle([f"Date: {format_d_m_Y(bilan.date)}"]) for title, text, html in ( ("Enfant(s)", '
'.join( [f"{enfant.nom_prenom} (*{format_d_m_Y(enfant.date_naissance)})" for enfant in bilan.famille.membres_suivis()]), False), ("Intervenant-e-s", ', '.join( [i.intervenant.nom_prenom for i in bilan.famille.interventions_actives(bilan.date)] ), False), ("Début du suivi", format_d_m_Y(bilan.famille.suivi.date_debut_suivi), False), ("Besoins et objectifs", bilan.objectifs, True), ("Rythme et fréquence", bilan.rythme, True), ): self.write_paragraph(title, text, html=html, justifie=True) self.story.append(Spacer(0, 0.5 * cm)) for idx, interv in enumerate(bilan.sig_interv.all()): if idx == 0: self.write_paragraph("Signature des intervenant-e-s AEMO :", '') self.story.append(Spacer(0, 0.2 * cm)) self.write_paragraph('', interv.nom_prenom + (f', {interv.profession}' if interv.profession else '')) self.story.append(Spacer(0, 1 * cm)) if bilan.sig_famille: self.write_paragraph("Signature de la famille :", '') self.doc.build(self.story, onFirstPage=self.draw_header_footer, onLaterPages=self.draw_footer) class CoordonneesPagePdf(CoordonneesFamillePdf): title = "Informations" def get_filename(self, famille): return '{}_aemo_evaluation.pdf'.format(slugify(famille.nom)) def str_or_empty(value): return '' if not value else str(value)