diff --git a/doc/plugins/badges.rst b/doc/plugins/badges.rst
new file mode 100644
index 0000000000..8b232215f1
--- /dev/null
+++ b/doc/plugins/badges.rst
@@ -0,0 +1,110 @@
+Badges
+======
+
+The badges plugin provides a HTTP API that exposes the various layouts used to generate PDF badges.
+
+Resource description
+--------------------
+
+The badge layout resource contains the following public fields:
+
+.. rst-class:: rest-resource-table
+
+===================================== ========================== =======================================================
+Field Type Description
+===================================== ========================== =======================================================
+id integer Internal layout ID
+name string Internal layout description
+default boolean ``true`` if this is the default layout
+layout object Layout specification for libpretixprint
+background URL Background PDF file
+item_assignments list of objects Products this layout is assigned to
+└ item integer Item ID
+===================================== ========================== =======================================================
+
+.. versionchanged:: 1.16
+
+ This resource has been added.
+
+
+Endpoints
+---------
+
+.. http:get:: /api/v1/organizers/(organizer)/events/(event)/badgelayouts/
+
+ Returns a list of all badge layouts
+
+ **Example request**:
+
+ .. sourcecode:: http
+
+ GET /api/v1/organizers/bigevents/events/democon/badgelayouts/ HTTP/1.1
+ Host: pretix.eu
+ Accept: application/json, text/javascript
+
+ **Example response**:
+
+ .. sourcecode:: http
+
+ HTTP/1.1 200 OK
+ Vary: Accept
+ Content-Type: text/javascript
+
+ {
+ "count": 1,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 1,
+ "name": "Default layout",
+ "default": true,
+ "layout": {…},
+ "background": {},
+ "item_assignments": []
+ }
+ ]
+ }
+
+ :query page: The page number in case of a multi-page result set, default is 1
+ :param organizer: The ``slug`` field of a valid organizer
+ :param event: The ``slug`` field of a valid event
+ :statuscode 200: no error
+ :statuscode 401: Authentication failure
+ :statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
+
+.. http:get:: /api/v1/organizers/(organizer)/events/(event)/badgelayouts/(id)/
+
+ Returns information on layout.
+
+ **Example request**:
+
+ .. sourcecode:: http
+
+ GET /api/v1/organizers/bigevents/events/democon/layoutsbadge/1/ HTTP/1.1
+ Host: pretix.eu
+ Accept: application/json, text/javascript
+
+ **Example response**:
+
+ .. sourcecode:: http
+
+ HTTP/1.1 200 OK
+ Vary: Accept
+ Content-Type: text/javascript
+
+ {
+ "id": 1,
+ "name": "Default layout",
+ "default": true,
+ "layout": {…},
+ "background": {},
+ "item_assignments": []
+ }
+
+ :param organizer: The ``slug`` field of the organizer to fetch
+ :param event: The ``slug`` field of the event to fetch
+ :param id: The ``id`` field of the layout to fetch
+ :statuscode 200: no error
+ :statuscode 401: Authentication failure
+ :statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.
diff --git a/doc/plugins/index.rst b/doc/plugins/index.rst
index eb5a4b8058..505ef6c786 100644
--- a/doc/plugins/index.rst
+++ b/doc/plugins/index.rst
@@ -12,3 +12,5 @@ If you want to **create** a plugin, please go to the
list
pretixdroid
banktransfer
+ ticketoutputpdf
+ badges
diff --git a/doc/plugins/ticketoutputpdf.rst b/doc/plugins/ticketoutputpdf.rst
new file mode 100644
index 0000000000..e73581b708
--- /dev/null
+++ b/doc/plugins/ticketoutputpdf.rst
@@ -0,0 +1,111 @@
+PDF ticket output
+=================
+
+The PDF ticket output plugin provides a HTTP API that exposes the various layouts used
+to generate PDF tickets.
+
+Resource description
+--------------------
+
+The ticket layout resource contains the following public fields:
+
+.. rst-class:: rest-resource-table
+
+===================================== ========================== =======================================================
+Field Type Description
+===================================== ========================== =======================================================
+id integer Internal layout ID
+name string Internal layout description
+default boolean ``true`` if this is the default layout
+layout object Layout specification for libpretixprint
+background URL Background PDF file
+item_assignments list of objects Products this layout is assigned to
+└ item integer Item ID
+===================================== ========================== =======================================================
+
+.. versionchanged:: 1.16
+
+ This resource has been added.
+
+
+Endpoints
+---------
+
+.. http:get:: /api/v1/organizers/(organizer)/events/(event)/ticketlayouts/
+
+ Returns a list of all ticket layouts
+
+ **Example request**:
+
+ .. sourcecode:: http
+
+ GET /api/v1/organizers/bigevents/events/democon/ticketlayouts/ HTTP/1.1
+ Host: pretix.eu
+ Accept: application/json, text/javascript
+
+ **Example response**:
+
+ .. sourcecode:: http
+
+ HTTP/1.1 200 OK
+ Vary: Accept
+ Content-Type: text/javascript
+
+ {
+ "count": 1,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 1,
+ "name": "Default layout",
+ "default": true,
+ "layout": {…},
+ "background": {},
+ "item_assignments": []
+ }
+ ]
+ }
+
+ :query page: The page number in case of a multi-page result set, default is 1
+ :param organizer: The ``slug`` field of a valid organizer
+ :param event: The ``slug`` field of a valid event
+ :statuscode 200: no error
+ :statuscode 401: Authentication failure
+ :statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
+
+.. http:get:: /api/v1/organizers/(organizer)/events/(event)/ticketlayouts/(id)/
+
+ Returns information on layout.
+
+ **Example request**:
+
+ .. sourcecode:: http
+
+ GET /api/v1/organizers/bigevents/events/democon/ticketlayouts/1/ HTTP/1.1
+ Host: pretix.eu
+ Accept: application/json, text/javascript
+
+ **Example response**:
+
+ .. sourcecode:: http
+
+ HTTP/1.1 200 OK
+ Vary: Accept
+ Content-Type: text/javascript
+
+ {
+ "id": 1,
+ "name": "Default layout",
+ "default": true,
+ "layout": {…},
+ "background": {},
+ "item_assignments": []
+ }
+
+ :param organizer: The ``slug`` field of the organizer to fetch
+ :param event: The ``slug`` field of the event to fetch
+ :param id: The ``id`` field of the layout to fetch
+ :statuscode 200: no error
+ :statuscode 401: Authentication failure
+ :statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.
diff --git a/doc/spelling_wordlist.txt b/doc/spelling_wordlist.txt
index 2cb55be786..b6e975c82b 100644
--- a/doc/spelling_wordlist.txt
+++ b/doc/spelling_wordlist.txt
@@ -43,6 +43,7 @@ inofficial
invalidations
iterable
Jimdo
+libpretixprint
libsass
linters
memcached
diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py
index 02449358af..7384d1d0d6 100644
--- a/src/pretix/api/serializers/order.py
+++ b/src/pretix/api/serializers/order.py
@@ -320,7 +320,7 @@ class CompatibleJSONField(serializers.JSONField):
def to_representation(self, value):
if value:
- return json.load(value)
+ return json.loads(value)
return value
diff --git a/src/pretix/base/pdf.py b/src/pretix/base/pdf.py
index 6bfd1a1142..babc379427 100644
--- a/src/pretix/base/pdf.py
+++ b/src/pretix/base/pdf.py
@@ -225,8 +225,11 @@ class Renderer:
qr_y = float(o['bottom']) * mm
renderPDF.draw(d, canvas, qr_x, qr_y)
+ def _get_ev(self, op, order):
+ return op.subevent or order.event
+
def _get_text_content(self, op: OrderPosition, order: Order, o: dict):
- ev = op.subevent or order.event
+ ev = self._get_ev(op, order)
if not o['content']:
return '(error)'
if o['content'] == 'other':
diff --git a/src/pretix/plugins/badges/api.py b/src/pretix/plugins/badges/api.py
new file mode 100644
index 0000000000..6d39524274
--- /dev/null
+++ b/src/pretix/plugins/badges/api.py
@@ -0,0 +1,30 @@
+from rest_framework import viewsets
+
+from pretix.api.serializers.i18n import I18nAwareModelSerializer
+from pretix.api.serializers.order import CompatibleJSONField
+
+from .models import BadgeItem, BadgeLayout
+
+
+class ItemAssignmentSerializer(I18nAwareModelSerializer):
+ class Meta:
+ model = BadgeItem
+ fields = ('item',)
+
+
+class BadgeLayoutSerializer(I18nAwareModelSerializer):
+ layout = CompatibleJSONField()
+ item_assignments = ItemAssignmentSerializer(many=True)
+
+ class Meta:
+ model = BadgeLayout
+ fields = ('id', 'name', 'default', 'layout', 'background', 'item_assignments')
+
+
+class BadgeLayoutViewSet(viewsets.ReadOnlyModelViewSet):
+ serializer_class = BadgeLayoutSerializer
+ queryset = BadgeLayout.objects.none()
+ lookup_field = 'id'
+
+ def get_queryset(self):
+ return self.request.event.badge_layouts.all()
diff --git a/src/pretix/plugins/badges/forms.py b/src/pretix/plugins/badges/forms.py
index 293912b066..151625c53e 100644
--- a/src/pretix/plugins/badges/forms.py
+++ b/src/pretix/plugins/badges/forms.py
@@ -19,6 +19,7 @@ class BadgeItemForm(forms.ModelForm):
event = kwargs.pop('event')
super().__init__(*args, **kwargs)
self.fields['layout'].label = _('Badge layout')
+ self.fields['layout'].empty_label = _('(Event default)')
self.fields['layout'].queryset = event.badge_layouts.all()
self.fields['layout'].required = False
diff --git a/src/pretix/plugins/badges/signals.py b/src/pretix/plugins/badges/signals.py
index ba1cbfe03b..f6e70feab1 100644
--- a/src/pretix/plugins/badges/signals.py
+++ b/src/pretix/plugins/badges/signals.py
@@ -70,6 +70,10 @@ def event_copy_data_receiver(sender, other, item_map, **kwargs):
bl.pk = None
bl.event = sender
bl.save()
+
+ if bl.background and bl.background.name:
+ bl.background.save('background.pdf', bl.background)
+
layout_map[oldid] = bl
for bi in BadgeItem.objects.filter(item__event=other):
diff --git a/src/pretix/plugins/badges/urls.py b/src/pretix/plugins/badges/urls.py
index 1459a18bf8..dca7740bd4 100644
--- a/src/pretix/plugins/badges/urls.py
+++ b/src/pretix/plugins/badges/urls.py
@@ -1,5 +1,8 @@
from django.conf.urls import url
+from pretix.api.urls import event_router
+from pretix.plugins.badges.api import BadgeLayoutViewSet
+
from .views import (
LayoutCreate, LayoutDelete, LayoutEditorView, LayoutListView,
LayoutSetDefault, OrderPrintDo,
@@ -19,3 +22,4 @@ urlpatterns = [
url(r'^control/event/(?P[^/]+)/(?P[^/]+)/badges/(?P\d+)/editor',
LayoutEditorView.as_view(), name='edit'),
]
+event_router.register('badgelayouts', BadgeLayoutViewSet)
diff --git a/src/pretix/plugins/ticketoutputpdf/api.py b/src/pretix/plugins/ticketoutputpdf/api.py
new file mode 100644
index 0000000000..76b2222337
--- /dev/null
+++ b/src/pretix/plugins/ticketoutputpdf/api.py
@@ -0,0 +1,31 @@
+from rest_framework import viewsets
+
+from pretix.api.serializers.i18n import I18nAwareModelSerializer
+from pretix.api.serializers.order import CompatibleJSONField
+
+from .models import TicketLayout, TicketLayoutItem
+
+
+class ItemAssignmentSerializer(I18nAwareModelSerializer):
+
+ class Meta:
+ model = TicketLayoutItem
+ fields = ('item',)
+
+
+class TicketLayoutSerializer(I18nAwareModelSerializer):
+ layout = CompatibleJSONField()
+ item_assignments = ItemAssignmentSerializer(many=True)
+
+ class Meta:
+ model = TicketLayout
+ fields = ('id', 'name', 'default', 'layout', 'background', 'item_assignments')
+
+
+class TicketLayoutViewSet(viewsets.ReadOnlyModelViewSet):
+ serializer_class = TicketLayoutSerializer
+ queryset = TicketLayout.objects.none()
+ lookup_field = 'id'
+
+ def get_queryset(self):
+ return self.request.event.ticket_layouts.all()
diff --git a/src/pretix/plugins/ticketoutputpdf/forms.py b/src/pretix/plugins/ticketoutputpdf/forms.py
new file mode 100644
index 0000000000..5613bc1602
--- /dev/null
+++ b/src/pretix/plugins/ticketoutputpdf/forms.py
@@ -0,0 +1,41 @@
+from django import forms
+from django.utils.translation import ugettext_lazy as _
+
+from pretix.base.models import CachedCombinedTicket, CachedTicket
+
+from .models import TicketLayout, TicketLayoutItem
+
+
+class TicketLayoutForm(forms.ModelForm):
+ class Meta:
+ model = TicketLayout
+ fields = ('name',)
+
+
+class TicketLayoutItemForm(forms.ModelForm):
+ class Meta:
+ model = TicketLayoutItem
+ fields = ('layout',)
+
+ def __init__(self, *args, **kwargs):
+ event = kwargs.pop('event')
+ super().__init__(*args, **kwargs)
+ self.fields['layout'].label = _('PDF ticket layout')
+ self.fields['layout'].empty_label = _('(Event default)')
+ self.fields['layout'].queryset = event.ticket_layouts.all()
+ self.fields['layout'].required = False
+
+ def save(self, commit=True):
+ if self.cleaned_data['layout'] is None:
+ if self.instance.pk:
+ self.instance.delete()
+ else:
+ return
+ else:
+ return super().save(commit=commit)
+ CachedTicket.objects.filter(
+ order_position__item_id=self.instance.item, provider='pdf'
+ ).delete()
+ CachedCombinedTicket.objects.filter(
+ order__positions__item=self.instance.item
+ ).delete()
diff --git a/src/pretix/plugins/ticketoutputpdf/migrations/0001_initial.py b/src/pretix/plugins/ticketoutputpdf/migrations/0001_initial.py
new file mode 100644
index 0000000000..aa2d720fd3
--- /dev/null
+++ b/src/pretix/plugins/ticketoutputpdf/migrations/0001_initial.py
@@ -0,0 +1,74 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.13 on 2018-06-05 13:21
+from __future__ import unicode_literals
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+import pretix.base.models.base
+import pretix.plugins.ticketoutputpdf.models
+
+
+class Migration(migrations.Migration):
+ initial = True
+
+ dependencies = [
+ ('pretixbase', '0095_auto_20180604_1129'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='TicketLayout',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('default', models.BooleanField(default=False, verbose_name='Default')),
+ ('name', models.CharField(max_length=190, verbose_name='Name')),
+ ('layout', models.TextField(
+ default='[{"italic": false, "bottom": "274.60", "align": "left", "fontfamily": "Open Sans", '
+ '"width": "175.00", "left": "17.50", "text": "Sample event name", "content": '
+ '"event_name", "fontsize": "16.0", "bold": false, "color": [0, 0, 0, 1], '
+ '"type": "textarea"}, {"italic": false, "bottom": "262.90", "align": "left", '
+ '"fontfamily": "Open Sans", "width": "110.00", "left": "17.50", "text": "Sample product '
+ '\\u2013 sample variation", "content": "itemvar", "fontsize": "13.0", "bold": false, '
+ '"color": [0, 0, 0, 1], "type": "textarea"}, {"italic": false, "bottom": "252.50", '
+ '"align": "left", "fontfamily": "Open Sans", "width": "110.00", "left": "17.50", '
+ '"text": "John Doe", "content": "attendee_name", "fontsize": "13.0", "bold": false, '
+ '"color": [0, 0, 0, 1], "type": "textarea"}, {"italic": false, "bottom": "242.10", '
+ '"align": "left", "fontfamily": "Open Sans", "width": "110.00", "left": "17.50", '
+ '"text": "May 31st, 2017", "content": "event_date_range", "fontsize": "13.0", '
+ '"bold": false, "color": [0, 0, 0, 1], "type": "textarea"}, {"italic": false, '
+ '"bottom": "204.80", "align": "left", "fontfamily": "Open Sans", "width": "110.00", '
+ '"left": "17.50", "text": "Random City", "content": "event_location", "fontsize": "13.0", '
+ '"bold": false, "color": [0, 0, 0, 1], "type": "textarea"}, {"italic": false, '
+ '"bottom": "194.50", "align": "left", "fontfamily": "Open Sans", "width": "30.00", '
+ '"left": "17.50", "text": "A1B2C", "content": "order", "fontsize": "13.0", "bold": false, '
+ '"color": [0, 0, 0, 1], "type": "textarea"}, {"italic": false, "bottom": "194.50", '
+ '"align": "right", "fontfamily": "Open Sans", "width": "45.00", "left": "52.50", '
+ '"text": "123.45 EUR", "content": "price", "fontsize": "13.0", "bold": false, '
+ '"color": [0, 0, 0, 1], "type": "textarea"}, {"italic": false, "bottom": "194.50", '
+ '"align": "left", "fontfamily": "Open Sans", "width": "90.00", "left": "102.50", '
+ '"text": "tdmruoekvkpbv1o2mv8xccvqcikvr58u", "content": "secret", "fontsize": "13.0", '
+ '"bold": false, "color": [0, 0, 0, 1], "type": "textarea"}, {"left": "130.40", '
+ '"bottom": "204.50", "type": "barcodearea", "size": "64.00"}]')),
+ ('background', models.FileField(blank=True, max_length=255, null=True,
+ upload_to=pretix.plugins.ticketoutputpdf.models.bg_name)),
+ ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticket_layouts',
+ to='pretixbase.Event')),
+ ],
+ options={
+ 'ordering': ('name',),
+ },
+ bases=(models.Model, pretix.base.models.base.LoggingMixin),
+ ),
+ migrations.CreateModel(
+ name='TicketLayoutItem',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('item', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
+ related_name='ticketlayout_assignment', to='pretixbase.Item')),
+ ('layout',
+ models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='item_assignments',
+ to='ticketoutputpdf.TicketLayout')),
+ ],
+ ),
+ ]
diff --git a/src/pretix/plugins/ticketoutputpdf/migrations/0002_auto_20180605_2022.py b/src/pretix/plugins/ticketoutputpdf/migrations/0002_auto_20180605_2022.py
new file mode 100644
index 0000000000..798a212ff8
--- /dev/null
+++ b/src/pretix/plugins/ticketoutputpdf/migrations/0002_auto_20180605_2022.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.13 on 2018-06-05 20:22
+from __future__ import unicode_literals
+
+from django.db import migrations
+from django.utils.translation import gettext
+
+from pretix.base.i18n import language
+
+
+def convert_old_settings(app, schema_editor):
+ EventSettingsStore = app.get_model('pretixbase', 'Event_SettingsStore')
+ for es in EventSettingsStore.objects.filter(key='ticketoutput_pdf_layout'):
+ locale_es = EventSettingsStore.objects.filter(object=es.object, key='locale').first()
+ if locale_es:
+ locale = locale_es.value
+ else:
+ locale = 'en'
+
+ with language(locale):
+ es.object.ticket_layouts.create(
+ name=gettext('Default layout'),
+ default=True,
+ layout=es.value
+ )
+
+ for es in EventSettingsStore.objects.filter(key='ticketoutput_pdf_background'):
+ locale_es = EventSettingsStore.objects.filter(object=es.object, key='locale').first()
+ if locale_es:
+ locale = locale_es.value
+ else:
+ locale = 'en'
+
+ with language(locale):
+ l = es.object.ticket_layouts.get_or_create(
+ default=True,
+ defaults={
+ 'name': gettext('Default layout'),
+ }
+ )[0]
+
+ l.background.name = es.value[7:]
+ setattr(l, 'background', l.background.name)
+ l.save()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('ticketoutputpdf', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.RunPython(convert_old_settings, migrations.RunPython.noop)
+ ]
diff --git a/src/pretix/plugins/ticketoutputpdf/migrations/__init__.py b/src/pretix/plugins/ticketoutputpdf/migrations/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/pretix/plugins/ticketoutputpdf/models.py b/src/pretix/plugins/ticketoutputpdf/models.py
new file mode 100644
index 0000000000..bbb191aa17
--- /dev/null
+++ b/src/pretix/plugins/ticketoutputpdf/models.py
@@ -0,0 +1,70 @@
+import string
+
+from django.db import models
+from django.utils.crypto import get_random_string
+from django.utils.translation import ugettext_lazy as _
+
+from pretix.base.models import LoggedModel
+
+
+def bg_name(instance, filename: str) -> str:
+ secret = get_random_string(length=16, allowed_chars=string.ascii_letters + string.digits)
+ return 'pub/{org}/{ev}/ticketoutputpdf/{id}-{secret}.pdf'.format(
+ org=instance.event.organizer.slug,
+ ev=instance.event.slug,
+ id=instance.pk,
+ secret=secret
+ )
+
+
+class TicketLayout(LoggedModel):
+ event = models.ForeignKey(
+ 'pretixbase.Event',
+ on_delete=models.CASCADE,
+ related_name='ticket_layouts'
+ )
+ default = models.BooleanField(
+ verbose_name=_('Default'),
+ default=False,
+ )
+ name = models.CharField(
+ max_length=190,
+ verbose_name=_('Name')
+ )
+ layout = models.TextField(
+ default='[{"italic": false, "bottom": "274.60", "align": "left", "fontfamily": "Open Sans", '
+ '"width": "175.00", "left": "17.50", "text": "Sample event name", "content": "event_name", '
+ '"fontsize": "16.0", "bold": false, "color": [0, 0, 0, 1], "type": "textarea"}, {"italic": false, '
+ '"bottom": "262.90", "align": "left", "fontfamily": "Open Sans", "width": "110.00", "left": "17.50", '
+ '"text": "Sample product \\u2013 sample variation", "content": "itemvar", "fontsize": "13.0", '
+ '"bold": false, "color": [0, 0, 0, 1], "type": "textarea"}, {"italic": false, "bottom": "252.50", '
+ '"align": "left", "fontfamily": "Open Sans", "width": "110.00", "left": "17.50", "text": "John Doe", '
+ '"content": "attendee_name", "fontsize": "13.0", "bold": false, "color": [0, 0, 0, 1], '
+ '"type": "textarea"}, {"italic": false, "bottom": "242.10", "align": "left", "fontfamily": "Open '
+ 'Sans", "width": "110.00", "left": "17.50", "text": "May 31st, 2017", "content": "event_date_range", '
+ '"fontsize": "13.0", "bold": false, "color": [0, 0, 0, 1], "type": "textarea"}, {"italic": false, '
+ '"bottom": "204.80", "align": "left", "fontfamily": "Open Sans", "width": "110.00", "left": "17.50", '
+ '"text": "Random City", "content": "event_location", "fontsize": "13.0", "bold": false, "color": [0, '
+ '0, 0, 1], "type": "textarea"}, {"italic": false, "bottom": "194.50", "align": "left", "fontfamily": '
+ '"Open Sans", "width": "30.00", "left": "17.50", "text": "A1B2C", "content": "order", "fontsize": '
+ '"13.0", "bold": false, "color": [0, 0, 0, 1], "type": "textarea"}, {"italic": false, '
+ '"bottom": "194.50", "align": "right", "fontfamily": "Open Sans", "width": "45.00", "left": "52.50", '
+ '"text": "123.45 EUR", "content": "price", "fontsize": "13.0", "bold": false, "color": [0, 0, 0, 1], '
+ '"type": "textarea"}, {"italic": false, "bottom": "194.50", "align": "left", "fontfamily": "Open '
+ 'Sans", "width": "90.00", "left": "102.50", "text": "tdmruoekvkpbv1o2mv8xccvqcikvr58u", "content": '
+ '"secret", "fontsize": "13.0", "bold": false, "color": [0, 0, 0, 1], "type": "textarea"}, '
+ '{"left": "130.40", "bottom": "204.50", "type": "barcodearea", "size": "64.00"}]'
+ )
+ background = models.FileField(null=True, blank=True, upload_to=bg_name, max_length=255)
+
+ class Meta:
+ ordering = ("name",)
+
+ def __str__(self):
+ return self.name
+
+
+class TicketLayoutItem(models.Model):
+ item = models.OneToOneField('pretixbase.Item', null=True, blank=True, related_name='ticketlayout_assignment',
+ on_delete=models.CASCADE)
+ layout = models.ForeignKey('TicketLayout', on_delete=models.CASCADE, related_name='item_assignments')
diff --git a/src/pretix/plugins/ticketoutputpdf/signals.py b/src/pretix/plugins/ticketoutputpdf/signals.py
index 4b1ba025fa..b54739ed97 100644
--- a/src/pretix/plugins/ticketoutputpdf/signals.py
+++ b/src/pretix/plugins/ticketoutputpdf/signals.py
@@ -1,12 +1,21 @@
+import copy
+import json
from functools import partial
from django.dispatch import receiver
+from django.urls import reverse
+from django.utils.html import escape
from django.utils.translation import ugettext_lazy as _
from pretix.base.models import QuestionAnswer
from pretix.base.signals import ( # NOQA: legacy import
- event_copy_data, layout_text_variables, register_data_exporters,
- register_ticket_outputs,
+ event_copy_data, item_copy_data, layout_text_variables, logentry_display,
+ logentry_object_link, register_data_exporters, register_ticket_outputs,
+)
+from pretix.control.signals import item_forms
+from pretix.plugins.ticketoutputpdf.forms import TicketLayoutItemForm
+from pretix.plugins.ticketoutputpdf.models import (
+ TicketLayout, TicketLayoutItem,
)
from pretix.presale.style import ( # NOQA: legacy import
get_fonts, register_fonts,
@@ -45,14 +54,88 @@ def variables_from_questions(sender, *args, **kwargs):
return d
-@receiver(signal=event_copy_data, dispatch_uid="pretix_ticketoutputpdf_copy_data")
-def event_copy_data_receiver(sender, other, question_map, **kwargs):
- layout = sender.settings.get('ticketoutput_pdf_layout', as_type=list)
- if not layout:
- return
- for o in layout:
- if o['type'] == 'textarea':
- if o['content'].startswith('question_'):
- o['content'] = 'question_{}'.format(question_map.get(int(o['content'][9:]), 0).pk)
+@receiver(item_forms, dispatch_uid="pretix_ticketoutputpdf_item_forms")
+def control_item_forms(sender, request, item, **kwargs):
+ try:
+ inst = TicketLayoutItem.objects.get(item=item)
+ except TicketLayoutItem.DoesNotExist:
+ inst = TicketLayoutItem(item=item)
+ return TicketLayoutItemForm(
+ instance=inst,
+ event=sender,
+ data=(request.POST if request.method == "POST" else None),
+ prefix="ticketlayoutitem"
+ )
- sender.settings.set('ticketoutput_pdf_layout', list(layout))
+
+@receiver(item_copy_data, dispatch_uid="pretix_ticketoutputpdf_item_copy")
+def copy_item(sender, source, target, **kwargs):
+ try:
+ inst = TicketLayoutItem.objects.get(item=source)
+ TicketLayoutItem.objects.create(item=target, layout=inst.layout)
+ except TicketLayoutItem.DoesNotExist:
+ pass
+
+
+@receiver(signal=event_copy_data, dispatch_uid="pretix_ticketoutputpdf_copy_data")
+def pdf_event_copy_data_receiver(sender, other, item_map, question_map, **kwargs):
+ if sender.ticket_layouts.exists(): # idempotency
+ return
+ layout_map = {}
+ for bl in other.ticket_layouts.all():
+ oldid = bl.pk
+ bl = copy.copy(bl)
+ bl.pk = None
+ bl.event = sender
+
+ layout = json.loads(bl.layout)
+ for o in layout:
+ if o['type'] == 'textarea':
+ if o['content'].startswith('question_'):
+ o['content'] = 'question_{}'.format(question_map.get(int(o['content'][9:]), 0).pk)
+ bl.layout = json.dumps(layout)
+
+ bl.save()
+
+ if bl.background and bl.background.name:
+ bl.background.save('background.pdf', bl.background)
+
+ layout_map[oldid] = bl
+
+ for bi in TicketLayoutItem.objects.filter(item__event=other):
+ TicketLayoutItem.objects.create(item=item_map.get(bi.item_id), layout=layout_map.get(bi.layout_id))
+ return layout_map
+
+
+@receiver(signal=logentry_display, dispatch_uid="pretix_ticketoutputpdf_logentry_display")
+def pdf_logentry_display(sender, logentry, **kwargs):
+ if not logentry.action_type.startswith('pretix.plugins.ticketoutputpdf'):
+ return
+
+ plains = {
+ 'pretix.plugins.ticketoutputpdf.layout.added': _('Ticket layout created.'),
+ 'pretix.plugins.ticketoutputpdf.layout.deleted': _('Ticket layout deleted.'),
+ 'pretix.plugins.ticketoutputpdf.layout.changed': _('Ticket layout changed.'),
+ }
+
+ if logentry.action_type in plains:
+ return plains[logentry.action_type]
+
+
+@receiver(signal=logentry_object_link, dispatch_uid="pretix_ticketoutputpdf_logentry_object_link")
+def pdf_logentry_object_link(sender, logentry, **kwargs):
+ if not logentry.action_type.startswith('pretix.plugins.ticketoutputpdf.layout') or not isinstance(
+ logentry.content_object, TicketLayout):
+ return
+
+ a_text = _('Ticket layout {val}')
+ a_map = {
+ 'href': reverse('plugins:ticketoutputpdf:edit', kwargs={
+ 'event': sender.slug,
+ 'organizer': sender.organizer.slug,
+ 'layout': logentry.content_object.id
+ }),
+ 'val': escape(logentry.content_object.name),
+ }
+ a_map['val'] = '{val}'.format_map(a_map)
+ return a_text.format_map(a_map)
diff --git a/src/pretix/plugins/ticketoutputpdf/templates/pretixplugins/ticketoutputpdf/delete.html b/src/pretix/plugins/ticketoutputpdf/templates/pretixplugins/ticketoutputpdf/delete.html
new file mode 100644
index 0000000000..1906c2fc2f
--- /dev/null
+++ b/src/pretix/plugins/ticketoutputpdf/templates/pretixplugins/ticketoutputpdf/delete.html
@@ -0,0 +1,20 @@
+{% extends "pretixcontrol/event/base.html" %}
+{% load i18n %}
+{% load bootstrap3 %}
+{% block title %}{% trans "Ticket layout" %}{% endblock %}
+{% block content %}
+ {% trans "Ticket layout" %}
+
+{% endblock %}
diff --git a/src/pretix/plugins/ticketoutputpdf/templates/pretixplugins/ticketoutputpdf/edit.html b/src/pretix/plugins/ticketoutputpdf/templates/pretixplugins/ticketoutputpdf/edit.html
new file mode 100644
index 0000000000..98db645c03
--- /dev/null
+++ b/src/pretix/plugins/ticketoutputpdf/templates/pretixplugins/ticketoutputpdf/edit.html
@@ -0,0 +1,39 @@
+{% extends "pretixcontrol/event/base.html" %}
+{% load i18n %}
+{% load bootstrap3 %}
+{% block title %}
+ {% if layout %}
+ {% blocktrans with name=layout.name %}Ticket layout: {{ name }}{% endblocktrans %}
+ {% else %}
+ {% trans "Ticket layout" %}
+ {% endif %}
+{% endblock %}
+{% block content %}
+ {% if layout %}
+ {% blocktrans with name=layout.name %}Ticket layout: {{ name }}{% endblocktrans %}
+ {% else %}
+ {% trans "Ticket layout" %}
+ {% endif %}
+
+{% endblock %}
diff --git a/src/pretix/plugins/ticketoutputpdf/templates/pretixplugins/ticketoutputpdf/form.html b/src/pretix/plugins/ticketoutputpdf/templates/pretixplugins/ticketoutputpdf/form.html
index 5c5b50d1eb..7ee6bee1dc 100644
--- a/src/pretix/plugins/ticketoutputpdf/templates/pretixplugins/ticketoutputpdf/form.html
+++ b/src/pretix/plugins/ticketoutputpdf/templates/pretixplugins/ticketoutputpdf/form.html
@@ -13,9 +13,13 @@
{% endblocktrans %}
+
+ {% trans "Change default layout in a new tab" %}
+
- {% trans "Open the PDF editor in a new tab" %}
+ href="{% url "plugins:ticketoutputpdf:index" organizer=request.organizer.slug event=request.event.slug %}">
+ {% trans "Advanced mode (multiple layouts)" %}
diff --git a/src/pretix/plugins/ticketoutputpdf/templates/pretixplugins/ticketoutputpdf/index.html b/src/pretix/plugins/ticketoutputpdf/templates/pretixplugins/ticketoutputpdf/index.html
new file mode 100644
index 0000000000..8fcc60dcdb
--- /dev/null
+++ b/src/pretix/plugins/ticketoutputpdf/templates/pretixplugins/ticketoutputpdf/index.html
@@ -0,0 +1,78 @@
+{% extends "pretixcontrol/event/base.html" %}
+{% load i18n %}
+{% load money %}
+{% block title %}{% trans "Ticket layouts" %}{% endblock %}
+{% block content %}
+ {% trans "Ticket layouts" %}
+ {% if layouts|length == 0 %}
+
+
+ {% blocktrans trimmed %}
+ You haven't created any layouts yet.
+ {% endblocktrans %}
+
+
+ {% if "can_change_event_settings" in request.eventpermset %}
+
{% trans "Create a new layout" %}
+
+ {% endif %}
+
+ {% else %}
+
+ {% if "can_change_event_settings" in request.eventpermset %}
+ {% trans "Create a new layout" %}
+
+ {% endif %}
+
+
+
+
+
+ | {% trans "Name" %} |
+ {% trans "Default" %} |
+ |
+
+
+
+ {% for l in layouts %}
+
+ |
+ {% if "can_change_event_settings" in request.eventpermset %}
+
+ {{ l.name }}
+
+ {% else %}
+ {{ l.name }}
+ {% endif %}
+ |
+
+ {% if l.default %}
+
+
+ {% trans "Default" %}
+
+ {% elif "can_change_event_settings" in request.eventpermset %}
+
+ {% endif %}
+ |
+
+ {% if "can_change_event_settings" in request.eventpermset %}
+
+
+ {% endif %}
+ |
+
+ {% endfor %}
+
+
+
+ {% endif %}
+ {% include "pretixcontrol/pagination.html" %}
+{% endblock %}
diff --git a/src/pretix/plugins/ticketoutputpdf/ticketoutput.py b/src/pretix/plugins/ticketoutputpdf/ticketoutput.py
index 8817856c05..e9d0f951ca 100644
--- a/src/pretix/plugins/ticketoutputpdf/ticketoutput.py
+++ b/src/pretix/plugins/ticketoutputpdf/ticketoutput.py
@@ -1,18 +1,25 @@
+import json
import logging
from io import BytesIO
from django.contrib.staticfiles import finders
from django.core.files import File
+from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
from django.http import HttpRequest
from django.template.loader import get_template
+from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
+from PyPDF2 import PdfFileMerger
from reportlab.pdfgen.canvas import Canvas
from pretix.base.i18n import language
from pretix.base.models import Order, OrderPosition
from pretix.base.pdf import Renderer
from pretix.base.ticketoutput import BaseTicketOutput
+from pretix.plugins.ticketoutputpdf.models import (
+ TicketLayout, TicketLayoutItem,
+)
logger = logging.getLogger('pretix.plugins.ticketoutputpdf')
@@ -27,35 +34,61 @@ class PdfTicketOutput(BaseTicketOutput):
self.override_background = override_background
super().__init__(event)
+ @cached_property
+ def layout_map(self):
+ return {
+ bi.item_id: bi.layout
+ for bi in TicketLayoutItem.objects.select_related('layout').filter(item__event=self.event)
+ }
+
+ @cached_property
+ def default_layout(self):
+ try:
+ return self.event.ticket_layouts.get(default=True)
+ except TicketLayout.DoesNotExist:
+ return TicketLayout(
+ layout=json.dumps(self._default_layout())
+ )
+
def _register_fonts(self):
Renderer._register_fonts()
- def _draw_page(self, canvas: Canvas, op: OrderPosition, order: Order):
- objs = self.override_layout or self.settings.get('layout', as_type=list) or self._legacy_layout()
+ def _draw_page(self, layout: TicketLayout, canvas: Canvas, op: OrderPosition, order: Order):
+ objs = self.override_layout or json.loads(layout.layout) or self._legacy_layout()
Renderer(self.event, objs, None).draw_page(canvas, order, op)
def generate_order(self, order: Order):
- buffer = BytesIO()
- p = self._create_canvas(buffer)
+ merger = PdfFileMerger()
with language(order.locale):
for op in order.positions.all():
if op.addon_to_id and not self.event.settings.ticket_download_addons:
continue
if not op.item.admission and not self.event.settings.ticket_download_nonadm:
continue
- self._draw_page(p, op, order)
- p.save()
- outbuffer = self._render_with_background(buffer)
+
+ buffer = BytesIO()
+ p = self._create_canvas(buffer)
+ layout = self.layout_map.get(op.item_id, self.default_layout)
+ self._draw_page(layout, p, op, order)
+ p.save()
+ outbuffer = self._render_with_background(layout, buffer)
+ merger.append(ContentFile(outbuffer.read()))
+
+ outbuffer = BytesIO()
+ merger.write(outbuffer)
+ merger.close()
+ outbuffer.seek(0)
return 'order%s%s.pdf' % (self.event.slug, order.code), 'application/pdf', outbuffer.read()
def generate(self, op):
buffer = BytesIO()
p = self._create_canvas(buffer)
order = op.order
+ layout = self.layout_map.get(op.item_id, self.default_layout)
with language(order.locale):
- self._draw_page(p, op, order)
+ self._draw_page(layout, p, op, order)
p.save()
- outbuffer = self._render_with_background(buffer)
+ outbuffer = self._render_with_background(layout, buffer)
return 'order%s%s.pdf' % (self.event.slug, order.code), 'application/pdf', outbuffer.read()
def _create_canvas(self, buffer):
@@ -71,11 +104,11 @@ class PdfTicketOutput(BaseTicketOutput):
def _get_default_background(self):
return open(finders.find('pretixpresale/pdf/ticket_default_a4.pdf'), "rb")
- def _render_with_background(self, buffer, title=_('Ticket')):
- bg_file = self.settings.get('background', as_type=File)
+ def _render_with_background(self, layout: TicketLayout, buffer, title=_('Ticket')):
+ bg_file = layout.background
if self.override_background:
bgf = default_storage.open(self.override_background.name, "rb")
- elif isinstance(bg_file, File):
+ elif isinstance(bg_file, File) and bg_file.name:
bgf = default_storage.open(bg_file.name, "rb")
else:
bgf = self._get_default_background()
diff --git a/src/pretix/plugins/ticketoutputpdf/urls.py b/src/pretix/plugins/ticketoutputpdf/urls.py
index f112ba293a..88892fae3b 100644
--- a/src/pretix/plugins/ticketoutputpdf/urls.py
+++ b/src/pretix/plugins/ticketoutputpdf/urls.py
@@ -1,8 +1,24 @@
from django.conf.urls import url
-from . import views
+from pretix.api.urls import event_router
+from pretix.plugins.ticketoutputpdf.api import TicketLayoutViewSet
+from pretix.plugins.ticketoutputpdf.views import (
+ LayoutCreate, LayoutDelete, LayoutEditorView, LayoutGetDefault,
+ LayoutListView, LayoutSetDefault,
+)
urlpatterns = [
- url(r'^control/event/(?P[^/]+)/(?P[^/]+)/pdfoutput/editor/$', views.EditorView.as_view(),
- name='editor'),
+ url(r'^control/event/(?P[^/]+)/(?P[^/]+)/pdfoutput/$',
+ LayoutListView.as_view(), name='index'),
+ url(r'^control/event/(?P[^/]+)/(?P[^/]+)/pdfoutput/add$',
+ LayoutCreate.as_view(), name='add'),
+ url(r'^control/event/(?P[^/]+)/(?P[^/]+)/pdfoutput/(?P\d+)/default$',
+ LayoutSetDefault.as_view(), name='default'),
+ url(r'^control/event/(?P[^/]+)/(?P[^/]+)/pdfoutput/default$',
+ LayoutGetDefault.as_view(), name='getdefault'),
+ url(r'^control/event/(?P[^/]+)/(?P[^/]+)/pdfoutput/(?P\d+)/delete$',
+ LayoutDelete.as_view(), name='delete'),
+ url(r'^control/event/(?P[^/]+)/(?P[^/]+)/pdfoutput/(?P\d+)/editor',
+ LayoutEditorView.as_view(), name='edit'),
]
+event_router.register('ticketlayouts', TicketLayoutViewSet)
diff --git a/src/pretix/plugins/ticketoutputpdf/views.py b/src/pretix/plugins/ticketoutputpdf/views.py
index ac75668525..304493a569 100644
--- a/src/pretix/plugins/ticketoutputpdf/views.py
+++ b/src/pretix/plugins/ticketoutputpdf/views.py
@@ -1,14 +1,34 @@
+import json
import logging
+from io import BytesIO
+from django.contrib import messages
+from django.contrib.staticfiles import finders
+from django.core.files import File
+from django.core.files.storage import default_storage
+from django.db import transaction
+from django.http import Http404
+from django.shortcuts import redirect
from django.templatetags.static import static
-from django.utils.translation import ugettext_lazy as _
+from django.urls import reverse
+from django.utils.functional import cached_property
+from django.utils.translation import gettext, ugettext_lazy as _
+from django.views import View
+from django.views.generic import CreateView, DeleteView, DetailView, ListView
+from reportlab.lib import pagesizes
+from reportlab.pdfgen import canvas
from pretix.base.models import (
- CachedCombinedTicket, CachedTicket, OrderPosition,
+ CachedCombinedTicket, CachedFile, CachedTicket, OrderPosition,
)
+from pretix.base.pdf import Renderer
+from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.views.pdf import BaseEditorView
+from pretix.plugins.ticketoutputpdf.forms import TicketLayoutForm
from pretix.plugins.ticketoutputpdf.ticketoutput import PdfTicketOutput
+from .models import TicketLayout
+
logger = logging.getLogger(__name__)
@@ -50,3 +70,192 @@ class EditorView(BaseEditorView):
self.request.event.settings.get(self.get_layout_settings_key(), as_type=list)
or prov._default_layout()
)
+
+
+class LayoutListView(EventPermissionRequiredMixin, ListView):
+ model = TicketLayout
+ permission = ('can_change_event_settings')
+ template_name = 'pretixplugins/ticketoutputpdf/index.html'
+ context_object_name = 'layouts'
+
+ def get_queryset(self):
+ return self.request.event.ticket_layouts.prefetch_related('item_assignments')
+
+
+class LayoutCreate(EventPermissionRequiredMixin, CreateView):
+ model = TicketLayout
+ form_class = TicketLayoutForm
+ template_name = 'pretixplugins/ticketoutputpdf/edit.html'
+ permission = 'can_change_event_settings'
+ context_object_name = 'layout'
+ success_url = '/ignored'
+
+ @transaction.atomic
+ def form_valid(self, form):
+ form.instance.event = self.request.event
+ if not self.request.event.ticket_layouts.filter(default=True).exists():
+ form.instance.default = True
+ messages.success(self.request, _('The new ticket layout has been created.'))
+ super().form_valid(form)
+ form.instance.log_action('pretix.plugins.ticketoutputpdf.layout.added', user=self.request.user,
+ data=dict(form.cleaned_data))
+ return redirect(reverse('plugins:ticketoutputpdf:edit', kwargs={
+ 'organizer': self.request.event.organizer.slug,
+ 'event': self.request.event.slug,
+ 'layout': form.instance.pk
+ }))
+
+ def form_invalid(self, form):
+ messages.error(self.request, _('We could not save your changes. See below for details.'))
+ return super().form_invalid(form)
+
+ def get_context_data(self, **kwargs):
+ return super().get_context_data(**kwargs)
+
+
+class LayoutSetDefault(EventPermissionRequiredMixin, DetailView):
+ model = TicketLayout
+ permission = 'can_change_event_settings'
+
+ def get_object(self, queryset=None) -> TicketLayout:
+ try:
+ return self.request.event.ticket_layouts.get(
+ id=self.kwargs['layout']
+ )
+ except TicketLayout.DoesNotExist:
+ raise Http404(_("The requested layout does not exist."))
+
+ @transaction.atomic
+ def post(self, request, *args, **kwargs):
+ messages.success(self.request, _('Your changes have been saved.'))
+ obj = self.get_object()
+ self.request.event.ticket_layouts.exclude(pk=obj.pk).update(default=False)
+ obj.default = True
+ obj.save(update_fields=['default'])
+ return redirect(self.get_success_url())
+
+ def get_success_url(self) -> str:
+ return reverse('plugins:ticketoutputpdf:index', kwargs={
+ 'organizer': self.request.event.organizer.slug,
+ 'event': self.request.event.slug,
+ })
+
+
+class LayoutDelete(EventPermissionRequiredMixin, DeleteView):
+ model = TicketLayout
+ template_name = 'pretixplugins/ticketoutputpdf/delete.html'
+ permission = 'can_change_event_settings'
+ context_object_name = 'layout'
+
+ def get_object(self, queryset=None) -> TicketLayout:
+ try:
+ return self.request.event.ticket_layouts.get(
+ id=self.kwargs['layout']
+ )
+ except TicketLayout.DoesNotExist:
+ raise Http404(_("The requested layout does not exist."))
+
+ @transaction.atomic
+ def delete(self, request, *args, **kwargs):
+ self.object = self.get_object()
+ self.object.log_action(action='pretix.plugins.ticketoutputpdf.layout.deleted', user=request.user)
+ self.object.delete()
+ if not self.request.event.ticket_layouts.filter(default=True).exists():
+ f = self.request.event.ticket_layouts.first()
+ if f:
+ f.default = True
+ f.save(update_fields=['default'])
+ messages.success(self.request, _('The selected ticket layout been deleted.'))
+ return redirect(self.get_success_url())
+
+ def get_success_url(self) -> str:
+ return reverse('plugins:ticketoutputpdf:index', kwargs={
+ 'organizer': self.request.event.organizer.slug,
+ 'event': self.request.event.slug,
+ })
+
+
+class LayoutGetDefault(EventPermissionRequiredMixin, View):
+ permission = 'can_change_event_settings'
+
+ def get(self, request, *args, **kwargs):
+ layout = self.request.event.ticket_layouts.get_or_create(
+ default=True,
+ defaults={
+ 'name': gettext('Default layout'),
+ }
+ )[0]
+ return redirect(reverse('plugins:ticketoutputpdf:edit', kwargs={
+ 'organizer': self.request.event.organizer.slug,
+ 'event': self.request.event.slug,
+ 'layout': layout.pk
+ }))
+
+
+class LayoutEditorView(BaseEditorView):
+
+ @cached_property
+ def layout(self):
+ try:
+ return self.request.event.ticket_layouts.get(
+ id=self.kwargs['layout']
+ )
+ except TicketLayout.DoesNotExist:
+ raise Http404(_("The requested layout does not exist."))
+
+ @property
+ def title(self):
+ return _('Ticket PDF layout: {}').format(self.layout)
+
+ def save_layout(self):
+ self.layout.layout = self.request.POST.get("data")
+ self.layout.save(update_fields=['layout'])
+ self.layout.log_action(action='pretix.plugins.ticketoutputpdf.layout.changed', user=self.request.user,
+ data={'layout': self.request.POST.get("data")})
+ CachedTicket.objects.filter(
+ order_position__order__event=self.request.event, provider='pdf'
+ ).delete()
+ CachedCombinedTicket.objects.filter(
+ order__event=self.request.event, provider='pdf'
+ ).delete()
+
+ def get_default_background(self):
+ return static('pretixpresale/pdf/ticket_default_a4.pdf')
+
+ def generate(self, op: OrderPosition, override_layout=None, override_background=None):
+ Renderer._register_fonts()
+
+ buffer = BytesIO()
+ if override_background:
+ bgf = default_storage.open(override_background.name, "rb")
+ elif isinstance(self.layout.background, File) and self.layout.background.name:
+ bgf = default_storage.open(self.layout.background.name, "rb")
+ else:
+ bgf = open(finders.find('pretixpresale/pdf/ticket_default_a4.pdf'), "rb")
+ r = Renderer(
+ self.request.event,
+ override_layout or self.get_current_layout(),
+ bgf,
+ )
+ p = canvas.Canvas(buffer, pagesize=pagesizes.A4)
+ r.draw_page(p, op.order, op)
+ p.save()
+ outbuffer = r.render_background(buffer, 'Ticket')
+ return 'ticket.pdf', 'application/pdf', outbuffer.read()
+
+ def get_current_layout(self):
+ return json.loads(self.layout.layout)
+
+ def get_current_background(self):
+ return self.layout.background.url if self.layout.background else self.get_default_background()
+
+ def save_background(self, f: CachedFile):
+ if self.layout.background:
+ self.layout.background.delete()
+ self.layout.background.save('background.pdf', f.file)
+ CachedTicket.objects.filter(
+ order_position__order__event=self.request.event, provider='pdf'
+ ).delete()
+ CachedCombinedTicket.objects.filter(
+ order__event=self.request.event, provider='pdf'
+ ).delete()
diff --git a/src/tests/plugins/badges/test_api.py b/src/tests/plugins/badges/test_api.py
new file mode 100644
index 0000000000..49cacf1177
--- /dev/null
+++ b/src/tests/plugins/badges/test_api.py
@@ -0,0 +1,61 @@
+import copy
+import json
+
+import pytest
+from django.utils.timezone import now
+
+from pretix.base.models import Event, Item, Organizer, Team, User
+from pretix.plugins.badges.models import BadgeItem
+
+
+@pytest.fixture
+def env():
+ o = Organizer.objects.create(name='Dummy', slug='dummy')
+ event = Event.objects.create(
+ organizer=o, name='Dummy', slug='dummy',
+ date_from=now(), plugins='pretix.plugins.banktransfer'
+ )
+ user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
+ t = Team.objects.create(organizer=event.organizer)
+ t.members.add(user)
+ t.limit_events.add(event)
+ item1 = Item.objects.create(event=event, name="Ticket", default_price=23)
+ tl = event.badge_layouts.create(name="Foo", default=True, layout='[{"a": 2}]')
+ BadgeItem.objects.create(layout=tl, item=item1)
+ return event, user, tl, item1
+
+
+RES_LAYOUT = {
+ 'id': 1,
+ 'name': 'Foo',
+ 'default': True,
+ 'item_assignments': [{'item': 1}],
+ 'layout': [{'a': 2}],
+ 'background': None
+}
+
+
+@pytest.mark.django_db
+def test_api_list(env, client):
+ res = copy.copy(RES_LAYOUT)
+ res['id'] = env[2].pk
+ res['item_assignments'][0]['item'] = env[3].pk
+ client.login(email='dummy@dummy.dummy', password='dummy')
+ r = json.loads(
+ client.get('/api/v1/organizers/{}/events/{}/badgelayouts/'.format(
+ env[0].slug, env[0].organizer.slug)).content.decode('utf-8')
+ )
+ assert r['results'] == [res]
+
+
+@pytest.mark.django_db
+def test_api_detail(env, client):
+ res = copy.copy(RES_LAYOUT)
+ res['id'] = env[2].pk
+ res['item_assignments'][0]['item'] = env[3].pk
+ client.login(email='dummy@dummy.dummy', password='dummy')
+ r = json.loads(
+ client.get('/api/v1/organizers/{}/events/{}/badgelayouts/{}/'.format(
+ env[0].slug, env[0].organizer.slug, env[2].pk)).content.decode('utf-8')
+ )
+ assert r == res
diff --git a/src/tests/plugins/ticketoutputpdf/__init__.py b/src/tests/plugins/ticketoutputpdf/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/tests/plugins/ticketoutputpdf/test_api.py b/src/tests/plugins/ticketoutputpdf/test_api.py
new file mode 100644
index 0000000000..4d51720ea3
--- /dev/null
+++ b/src/tests/plugins/ticketoutputpdf/test_api.py
@@ -0,0 +1,61 @@
+import copy
+import json
+
+import pytest
+from django.utils.timezone import now
+
+from pretix.base.models import Event, Item, Organizer, Team, User
+from pretix.plugins.ticketoutputpdf.models import TicketLayoutItem
+
+
+@pytest.fixture
+def env():
+ o = Organizer.objects.create(name='Dummy', slug='dummy')
+ event = Event.objects.create(
+ organizer=o, name='Dummy', slug='dummy',
+ date_from=now(), plugins='pretix.plugins.banktransfer'
+ )
+ user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
+ t = Team.objects.create(organizer=event.organizer)
+ t.members.add(user)
+ t.limit_events.add(event)
+ item1 = Item.objects.create(event=event, name="Ticket", default_price=23)
+ tl = event.ticket_layouts.create(name="Foo", default=True, layout='[{"a": 2}]')
+ TicketLayoutItem.objects.create(layout=tl, item=item1)
+ return event, user, tl, item1
+
+
+RES_LAYOUT = {
+ 'id': 1,
+ 'name': 'Foo',
+ 'default': True,
+ 'item_assignments': [{'item': 1}],
+ 'layout': [{'a': 2}],
+ 'background': None
+}
+
+
+@pytest.mark.django_db
+def test_api_list(env, client):
+ res = copy.copy(RES_LAYOUT)
+ res['id'] = env[2].pk
+ res['item_assignments'][0]['item'] = env[3].pk
+ client.login(email='dummy@dummy.dummy', password='dummy')
+ r = json.loads(
+ client.get('/api/v1/organizers/{}/events/{}/ticketlayouts/'.format(
+ env[0].slug, env[0].organizer.slug)).content.decode('utf-8')
+ )
+ assert r['results'] == [res]
+
+
+@pytest.mark.django_db
+def test_api_detail(env, client):
+ res = copy.copy(RES_LAYOUT)
+ res['id'] = env[2].pk
+ res['item_assignments'][0]['item'] = env[3].pk
+ client.login(email='dummy@dummy.dummy', password='dummy')
+ r = json.loads(
+ client.get('/api/v1/organizers/{}/events/{}/ticketlayouts/{}/'.format(
+ env[0].slug, env[0].organizer.slug, env[2].pk)).content.decode('utf-8')
+ )
+ assert r == res
diff --git a/src/tests/plugins/ticketoutputpdf/test_control.py b/src/tests/plugins/ticketoutputpdf/test_control.py
new file mode 100644
index 0000000000..9dd8bcd633
--- /dev/null
+++ b/src/tests/plugins/ticketoutputpdf/test_control.py
@@ -0,0 +1,125 @@
+import datetime
+
+from tests.base import SoupTest, extract_form_fields
+
+from pretix.base.models import Event, Item, Organizer, Team, User
+from pretix.plugins.ticketoutputpdf.models import TicketLayoutItem
+
+
+class TicketLayoutFormTest(SoupTest):
+ def setUp(self):
+ super().setUp()
+ self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
+ self.orga1 = Organizer.objects.create(name='CCC', slug='ccc')
+ self.orga2 = Organizer.objects.create(name='MRM', slug='mrm')
+ self.event1 = Event.objects.create(
+ organizer=self.orga1, name='30C3', slug='30c3',
+ plugins='pretix.plugins.ticketoutputpdf',
+ date_from=datetime.datetime(2013, 12, 26, tzinfo=datetime.timezone.utc),
+ )
+ self.item1 = Item.objects.create(event=self.event1, name="Standard", default_price=0, position=1)
+ t = Team.objects.create(organizer=self.orga1, can_change_event_settings=True, can_view_orders=True,
+ can_change_items=True, all_events=True, can_create_events=True)
+ t.members.add(self.user)
+ t.limit_events.add(self.event1)
+ self.client.login(email='dummy@dummy.dummy', password='dummy')
+
+ def test_create(self):
+ doc = self.get_doc('/control/event/%s/%s/pdfoutput/add' % (self.orga1.slug, self.event1.slug))
+ form_data = extract_form_fields(doc.select('.container-fluid form')[0])
+ form_data['name'] = 'Layout 1'
+ doc = self.post_doc('/control/event/%s/%s/pdfoutput/add' % (self.orga1.slug, self.event1.slug), form_data)
+ assert doc.select(".alert-success")
+ self.assertIn("Layout 1", doc.select("#page-wrapper")[0].text)
+ assert self.event1.ticket_layouts.get(
+ default=True, name='Layout 1'
+ )
+
+ def test_set_default(self):
+ bl1 = self.event1.ticket_layouts.create(name="Layout 1", default=True)
+ bl2 = self.event1.ticket_layouts.create(name="Layout 2")
+ self.post_doc('/control/event/%s/%s/pdfoutput/%s/default' % (self.orga1.slug, self.event1.slug, bl2.id), {})
+ bl1.refresh_from_db()
+ assert not bl1.default
+ bl2.refresh_from_db()
+ assert bl2.default
+
+ def test_delete(self):
+ bl1 = self.event1.ticket_layouts.create(name="Layout 1", default=True)
+ bl2 = self.event1.ticket_layouts.create(name="Layout 2")
+ doc = self.get_doc('/control/event/%s/%s/pdfoutput/%s/delete' % (self.orga1.slug, self.event1.slug, bl1.id))
+ form_data = extract_form_fields(doc.select('.container-fluid form')[0])
+ doc = self.post_doc('/control/event/%s/%s/pdfoutput/%s/delete' % (self.orga1.slug, self.event1.slug, bl1.id),
+ form_data)
+ assert doc.select(".alert-success")
+ self.assertNotIn("Layout 1", doc.select("#page-wrapper")[0].text)
+ assert self.event1.ticket_layouts.count() == 1
+ bl2.refresh_from_db()
+ assert bl2.default
+
+ def test_set_on_item(self):
+ self.event1.ticket_layouts.create(name="Layout 1", default=True)
+ bl2 = self.event1.ticket_layouts.create(name="Layout 2")
+ self.client.post('/control/event/%s/%s/items/%d/' % (self.orga1.slug, self.event1.slug, self.item1.id), {
+ 'name_0': 'Standard',
+ 'default_price': '23.00',
+ 'tax_rate': '19.00',
+ 'active': 'yes',
+ 'allow_cancel': 'yes',
+ 'ticketlayoutitem-layout': bl2.pk
+ })
+ assert TicketLayoutItem.objects.get(item=self.item1, layout=bl2)
+ self.client.post('/control/event/%s/%s/items/%d/' % (self.orga1.slug, self.event1.slug, self.item1.id), {
+ 'name_0': 'Standard',
+ 'default_price': '23.00',
+ 'tax_rate': '19.00',
+ 'active': 'yes',
+ 'allow_cancel': 'yes',
+ })
+ assert not TicketLayoutItem.objects.filter(item=self.item1, layout=bl2).exists()
+
+ def test_item_copy(self):
+ bl2 = self.event1.ticket_layouts.create(name="Layout 2")
+ TicketLayoutItem.objects.create(item=self.item1, layout=bl2)
+ self.client.post('/control/event/%s/%s/items/add' % (self.orga1.slug, self.event1.slug), {
+ 'name_0': 'Intermediate',
+ 'default_price': '23.00',
+ 'tax_rate': '19.00',
+ 'copy_from': str(self.item1.pk),
+ 'has_variations': '1'
+ })
+ i_new = Item.objects.get(name__icontains='Intermediate')
+ assert TicketLayoutItem.objects.get(item=i_new, layout=bl2)
+ assert TicketLayoutItem.objects.get(item=self.item1, layout=bl2)
+
+ def test_copy_event(self):
+ bl2 = self.event1.ticket_layouts.create(name="Layout 2")
+ TicketLayoutItem.objects.create(item=self.item1, layout=bl2)
+ self.post_doc('/control/events/add', {
+ 'event_wizard-current_step': 'foundation',
+ 'foundation-organizer': self.orga1.pk,
+ 'foundation-locales': ('en',)
+ })
+ self.post_doc('/control/events/add', {
+ 'event_wizard-current_step': 'basics',
+ 'basics-name_0': '33C3',
+ 'basics-slug': '33c3',
+ 'basics-date_from_0': '2016-12-27',
+ 'basics-date_from_1': '10:00:00',
+ 'basics-date_to_0': '2016-12-30',
+ 'basics-date_to_1': '19:00:00',
+ 'basics-location_0': 'Hamburg',
+ 'basics-currency': 'EUR',
+ 'basics-tax_rate': '19.00',
+ 'basics-locale': 'en',
+ 'basics-timezone': 'Europe/Berlin',
+ })
+ self.post_doc('/control/events/add', {
+ 'event_wizard-current_step': 'copy',
+ 'copy-copy_from_event': self.event1.pk
+ })
+
+ ev = Event.objects.get(slug='33c3')
+ i_new = ev.items.first()
+ bl_new = ev.ticket_layouts.first()
+ assert TicketLayoutItem.objects.get(item=i_new, layout=bl_new)
diff --git a/src/tests/plugins/test_ticketoutputpdf.py b/src/tests/plugins/ticketoutputpdf/test_ticketoutputpdf.py
similarity index 96%
rename from src/tests/plugins/test_ticketoutputpdf.py
rename to src/tests/plugins/ticketoutputpdf/test_ticketoutputpdf.py
index ff1a060f16..9b6584a598 100644
--- a/src/tests/plugins/test_ticketoutputpdf.py
+++ b/src/tests/plugins/ticketoutputpdf/test_ticketoutputpdf.py
@@ -13,7 +13,7 @@ from pretix.plugins.ticketoutputpdf.ticketoutput import PdfTicketOutput
@pytest.fixture
-def env():
+def env0():
o = Organizer.objects.create(name='Dummy', slug='dummy')
event = Event.objects.create(
organizer=o, name='Dummy', slug='dummy',
@@ -39,8 +39,8 @@ def env():
@pytest.mark.django_db
-def test_generate_pdf(env):
- event, order = env
+def test_generate_pdf(env0):
+ event, order = env0
event.settings.set('ticketoutput_pdf_code_x', 30)
event.settings.set('ticketoutput_pdf_code_y', 50)
event.settings.set('ticketoutput_pdf_code_s', 2)