Repository: ephios-dev/ephios

Issue:
PDF-Export für Events
Als Verantwortlicher für ein Event möchte ich einen PDF-Export des Events anstoßen können. Dabei werden alle relevanten Informationen zum Event und zu den Schichten aufgelistet. Insbesondere wird auch eine Auflistung der Sections (falls vorhanden) und eine Liste bzw. Zuteilung der Helfer erstellt. Nicht besetzte Positionen bekommen dabei ein leeres Feld, um eine Eintragung in Papierform zu ermöglichen. 

Export der alten Software:
[KTW (Zusatz).pdf](https://github.com/jeriox/jep/files/5081462/KTW.Zusatz.pdf)



Discussion:

Resolving patch:
diff --git a/event_management/pdf.py b/event_management/pdf.py
new file mode 100644
--- /dev/null
+++ b/event_management/pdf.py
@@ -0,0 +1,149 @@
+import io
+
+import pytz
+from django.http import FileResponse
+from django.utils import formats
+from django.views import View
+from django.views.generic.detail import SingleObjectMixin
+from guardian.mixins import PermissionRequiredMixin
+from reportlab.lib.pagesizes import A5, A4
+from reportlab.lib.styles import getSampleStyleSheet
+from reportlab.lib.units import cm
+from reportlab.platypus import Paragraph, SimpleDocTemplate, Table, Spacer
+from django.utils.translation import gettext as _
+
+from event_management.models import Event
+from jep import settings
+
+
+class BasePDFExporter:
+    def __init__(self, title, style=getSampleStyleSheet(), pagesize=A4):
+        self.title = title
+        self.style = style
+        self.pagesize = pagesize
+
+    def get_pdf(self):
+        buffer = io.BytesIO()
+        story = self.get_story()
+        p = SimpleDocTemplate(
+            buffer,
+            pagesize=self.pagesize,
+            title=self.title,
+            leftMargin=1 * cm,
+            rightMargin=1 * cm,
+            topMargin=1 * cm,
+            bottomMargin=1 * cm,
+        )
+        p.build(story)
+        buffer.seek(0)
+        return FileResponse(buffer, as_attachment=True, filename=f"{self.title}.pdf")
+
+    def get_story(self):
+        return NotImplemented
+
+
+class SingleShiftEventExporter(BasePDFExporter):
+    def __init__(self, event, **kwargs):
+        self.event = event
+        super().__init__(title=event.title, pagesize=A5)
+
+    def get_story(self):
+        story = [
+            Paragraph(f"{self.event.type}: {self.event.title}", self.style["Heading1"]),
+            Spacer(height=0.5 * cm, width=15 * cm),
+        ]
+
+        shift = self.event.shifts.first()
+        tz = pytz.timezone(settings.TIME_ZONE)
+        start_time = shift.start_time.astimezone(tz)
+        data = (
+            [
+                [_("Location"), self.event.location],
+                [
+                    _("Date"),
+                    f"{formats.date_format(start_time, 'l')}, {formats.date_format(start_time, 'SHORT_DATE_FORMAT')}",
+                ],
+                [
+                    _("Time"),
+                    f"{formats.time_format(start_time)} - {formats.time_format(shift.end_time.astimezone(tz))}",
+                ],
+                [_("Meeting time"), formats.time_format(shift.meeting_time.astimezone(tz))],
+            ]
+            + [[key, value] for key, value in shift.signup_method.get_signup_info().items()]
+            + [[_("Description"), Paragraph(self.event.description)]]
+        )
+        table = Table(data, colWidths=[3.7 * cm, 9 * cm])
+        table.setStyle([("VALIGN", (0, -1), (-1, -1), "TOP")])
+        story.append(table)
+        story.append(Spacer(height=0.5 * cm, width=15 * cm))
+
+        data = [
+            [f"{participator.first_name} {participator.last_name}"]
+            for participator in shift.get_participators()
+        ]
+        if data:
+            story.append(Paragraph(_("Participants"), self.style["Heading2"]))
+            story.append(Table(data, colWidths=[12.7 * cm]))
+
+        return story
+
+
+class MultipleShiftEventExporter(BasePDFExporter):
+    def __init__(self, event):
+        self.event = event
+        super().__init__(title=event.title)
+
+    def get_story(self):
+        story = [
+            Paragraph(f"{self.event.type}: {self.event.title}", self.style["Heading1"]),
+            Spacer(height=0.5 * cm, width=19 * cm),
+        ]
+
+        tz = pytz.timezone(settings.TIME_ZONE)
+        start_time = self.event.start_time.astimezone(tz)
+        end_time = self.event.end_time.astimezone(tz)
+        end_date = (
+            f"- {formats.date_format(end_time, 'l')}, {formats.date_format(end_time, 'SHORT_DATE_FORMAT')}"
+            if end_time.date() > start_time.date()
+            else ""
+        )
+        event_date = f"{formats.date_format(start_time, 'l')}, {formats.date_format(start_time, 'SHORT_DATE_FORMAT')} {end_date}"
+        data = [
+            [_("Location"), self.event.location],
+            [_("Date"), event_date],
+            [_("Description"), Paragraph(self.event.description)],
+        ]
+        table = Table(data, colWidths=[6 * cm, 13 * cm])
+        table.setStyle([("VALIGN", (0, -1), (-1, -1), "TOP")])
+        story.append(table)
+
+        for shift in self.event.shifts.all():
+            story.append(Spacer(height=1 * cm, width=19 * cm))
+            story.append(Paragraph(shift.get_start_end_time_display(), self.style["Heading2"]))
+            data = [
+                [_("Meeting time"), formats.time_format(shift.meeting_time.astimezone(tz))],
+            ] + [[key, value] for key, value in shift.signup_method.get_signup_info().items()]
+            story.append(Table(data, colWidths=[6 * cm, 13 * cm]))
+
+            data = [
+                [f"{participator.first_name} {participator.last_name}"]
+                for participator in shift.get_participators()
+            ]
+            if data:
+                story.append(Paragraph(_("Participants"), self.style["Heading3"]))
+                story.append(Table(data, colWidths=[19 * cm]))
+
+        return story
+
+
+class EventDetailPDFView(PermissionRequiredMixin, SingleObjectMixin, View):
+    permission_required = "event_management.view_event"
+    accept_global_perms = True
+    model = Event
+
+    def get(self, request, *args, **kwargs):
+        event = self.get_object()
+        if event.shifts.count() > 1:
+            return MultipleShiftEventExporter(event=event).get_pdf()
+        else:
+            return SingleShiftEventExporter(event=event).get_pdf()
diff --git a/event_management/urls.py b/event_management/urls.py
--- a/event_management/urls.py
+++ b/event_management/urls.py
@@ -1,7 +1,7 @@
 from django.urls import path
 
-from event_management import views
-from event_management.ical import EventFeed, UserEventFeed, user_event_feed_view
+from event_management import views, pdf
+from event_management.ical import EventFeed, user_event_feed_view
 from event_management.views import ShiftConfigurationFormView
 
 app_name = "event_management"
@@ -13,9 +13,10 @@
     path("events/<int:pk>/", views.EventDetailView.as_view(), name="event_detail"),
     path("events/<int:pk>/createshift/", views.ShiftCreateView.as_view(), name="event_createshift"),
     path("events/<int:pk>/activate/", views.EventActivateView.as_view(), name="event_activate"),
+    path("events/<int:pk>/pdf/", pdf.EventDetailPDFView.as_view(), name="event_detail_pdf"),
     path("events/create/", views.EventCreateView.as_view(), name="event_create"),
-    path("shifts/<int:pk>/signup-action/", views.ShiftSignupView.as_view(), name="shift_action",),
     path("events/past/", views.EventArchiveView.as_view(), name="event_list_past"),
+    path("shifts/<int:pk>/signup-action/", views.ShiftSignupView.as_view(), name="shift_action",),
     path("shifts/<int:pk>/edit/", views.ShiftUpdateView.as_view(), name="shift_edit",),
     path("shifts/<int:pk>/delete/", views.ShiftDeleteView.as_view(), name="shift_delete",),
     path(


Test patch: