from tempfile import NamedTemporaryFile from django.utils.timezone import now from pretalx.schedule.exporters import ScheduleData from reportlab.lib import colors from reportlab.lib.enums import TA_CENTER from reportlab.lib.pagesizes import A4 from reportlab.lib.styles import ParagraphStyle, StyleSheet1 from reportlab.lib.units import mm from reportlab.platypus import ( Flowable, PageBreak, Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle, ) from pretalx_broadcast_tools.utils.placeholders import placeholders A4_WIDTH, A4_HEIGHT = A4 PAGE_PADDING = 10 * mm class PDFInfoPage(Flowable): def __init__(self, event, schedule, fahrplan_day, room_details, talk, style): super().__init__() self.event = event self.schedule = schedule self.day = fahrplan_day self.room = room_details self.talk = talk self.style = style self.y_position = PAGE_PADDING @property def _questions(self): return { int(i.strip()) for i in self.event.settings.broadcast_tools_pdf_questions_to_include.split( "," ) if i } def _add(self, item, gap=2): _, height = item.wrapOn( self.canv, A4_WIDTH - 2 * PAGE_PADDING, A4_HEIGHT - 2 * PAGE_PADDING ) self.y_position += height + gap * mm item.drawOn(self.canv, PAGE_PADDING, -self.y_position) def _checkbox_text(self, text, **kwargs): item = Paragraph(text, **kwargs) _, height = item.wrapOn( self.canv, A4_WIDTH - 2 * PAGE_PADDING, A4_HEIGHT - 2 * PAGE_PADDING ) self.y_position += height + 2 * mm item.drawOn(self.canv, PAGE_PADDING + 1.3 * height, -self.y_position) self.canv.rect(PAGE_PADDING, -self.y_position, height * 0.8, height * 0.8) def _question_text(self, question, answer, **kwargs): item = Paragraph(question, **kwargs) _, height = item.wrapOn( self.canv, A4_WIDTH - 2 * PAGE_PADDING, A4_HEIGHT - 2 * PAGE_PADDING ) self.y_position += height + 2 * mm item.drawOn(self.canv, PAGE_PADDING, -self.y_position) item = Paragraph(answer, **kwargs) _, height = item.wrapOn( self.canv, A4_WIDTH - 3 * PAGE_PADDING, A4_HEIGHT - 2 * PAGE_PADDING ) self.y_position += height + 2 * mm item.drawOn(self.canv, 2 * PAGE_PADDING, -self.y_position) def _space(self): self._add(Spacer(1, PAGE_PADDING / 2)) def draw(self): if hasattr(self.talk, "local_start"): talk_start = self.talk.local_start else: talk_start = self.talk.start.astimezone(self.event.tz) if hasattr(self.talk, "local_end"): talk_end = self.talk.local_end else: talk_end = self.talk.end.astimezone(self.event.tz) # add some information horizontally to the side of the page self.canv.saveState() self.canv.rotate(90) self.canv.setFont("Helvetica", 12) self.canv.drawString( -(A4_HEIGHT - (PAGE_PADDING / 3)), -(PAGE_PADDING / 3), " | ".join( [ self.talk.submission.code, self.talk.submission.submission_type.name.localize( self.event.locale ), self.event.name.localize(self.event.locale), talk_start.isoformat(), f"Day {self.day['index']}", self.room["name"].localize(self.event.locale), f"Schedule {self.schedule.version}", ], ), ) self.canv.restoreState() if self.talk.submission.do_not_record: self._add( Paragraph("DO NOT RECORD - DO NOT STREAM", style=self.style["Warning"]), gap=0, ) self._space() self._add( Paragraph( " | ".join( [ self.event.name.localize(self.event.locale), self.room["name"].localize(self.event.locale), talk_start.strftime("%F"), f'{talk_start.strftime("%T")} - {talk_end.strftime("%T")}', ], ), style=self.style["Meta"], ) ) self._add( Paragraph(self.talk.submission.title, style=self.style["Title"]), gap=0 ) self._space() for spk in self.talk.submission.speakers.all(): self._checkbox_text( spk.get_display_name(), style=self.style["Speaker"], ) self._space() self._add( Table( [ ( "Duration", "Language", "Type", "Code", ), ( self.talk.export_duration, self.talk.submission.content_locale, self.talk.submission.submission_type.name, self.talk.submission.code, ), ], colWidths=30 * mm, style=TableStyle( [ ("ALIGN", (0, 0), (-1, -1), "CENTER"), ("VALIGN", (0, 0), (-1, -1), "TOP"), ("INNERGRID", (0, 0), (-1, -1), 0.25, colors.black), ("BOX", (0, 0), (-1, -1), 0.25, colors.black), ] ), ) ) if self.talk.submission.abstract: self._add( Paragraph( self.talk.submission.abstract, style=self.style["Abstract"], ) ) if self.event.settings.broadcast_tools_pdf_additional_content: self._space() self._add( Paragraph( self.event.settings.broadcast_tools_pdf_additional_content.format( **placeholders(self.schedule, self.talk) ), style=self.style["Meta"], ) ) if self.talk.submission.answers and self._questions: self._space() self._add( Paragraph( "Questions", style=self.style["Heading"], ) ) for answer in self.talk.submission.answers.order_by("question"): if answer.question.id not in self._questions: continue self._question_text( answer.question.question.localize(self.event.locale), answer.answer, style=self.style["Question"], ) if self.talk.submission.notes: self._space() self._add( Paragraph( "Notes", style=self.style["Heading"], ) ) for line in self.talk.submission.notes.splitlines(): line = line.strip() if not line: continue self._add( Paragraph( line, style=self.style["Notes"], ) ) if ( self.talk.submission.internal_notes and self.event.settings.broadcast_tools_pdf_show_internal_notes ): self._space() self._add( Paragraph( "Internal Notes", style=self.style["Heading"], ) ) for line in self.talk.submission.internal_notes.splitlines(): line = line.strip() if not line: continue self._add( Paragraph( line, style=self.style["Notes"], ) ) class PDFExporter(ScheduleData): identifier = "broadcast_pdf" verbose_name = "Broadcast Tools PDF" public = False show_qrcode = False icon = "fa-file-pdf" def _add_pages(self, doc): style = self._style() pages = [] for fahrplan_day in self.data: for room_details in fahrplan_day["rooms"]: for talk in room_details["talks"]: if ( talk.submission.do_not_record and self.event.settings.broadcast_tools_pdf_ignore_do_not_record ): continue pages.append( PDFInfoPage( self.event, self.schedule, fahrplan_day, room_details, talk, style, ) ) pages.append(PageBreak()) return pages def _style(self): stylesheet = StyleSheet1() stylesheet.add( ParagraphStyle(name="Normal", fontName="Helvetica", fontSize=12, leading=14) ) stylesheet.add( ParagraphStyle( name="Title", fontName="Helvetica-Bold", fontSize=20, leading=24 ) ) stylesheet.add( ParagraphStyle( name="Speaker", fontName="Helvetica-Oblique", fontSize=12, leading=14 ) ) stylesheet.add( ParagraphStyle(name="Meta", fontName="Helvetica", fontSize=14, leading=16) ) stylesheet.add( ParagraphStyle( name="Heading", fontName="Helvetica-Bold", fontSize=14, leading=16 ) ) stylesheet.add( ParagraphStyle( name="Question", fontName="Helvetica", fontSize=12, leading=14 ) ) stylesheet.add( ParagraphStyle( name="Abstract", fontName="Helvetica-Oblique", fontSize=10, leading=12 ) ) stylesheet.add( ParagraphStyle(name="Notes", fontName="Helvetica", fontSize=12, leading=14) ) stylesheet.add( ParagraphStyle( name="Warning", fontName="Helvetica-Bold", fontSize=20, leading=24, alignment=TA_CENTER, textColor=colors.red, ) ) return stylesheet def render(self, *args, **kwargs): with NamedTemporaryFile(suffix=".pdf") as f: doc = SimpleDocTemplate( f.name, pagesize=A4, rightMargin=0, leftMargin=0, topMargin=0, bottomMargin=0, ) doc.build(self._add_pages(doc)) f.seek(0) timestamp = now().strftime("%Y-%m-%d-%H%M") return ( f"{self.event.slug}_broadcast_tools_{timestamp}.pdf", "application/pdf", f.read(), )