Fix #549 -- Multiple PDF ticket layouts (#938)

- [x] Data model
- [x] CRUD
- [x] Editor
- [x] Migration from old settings
- [x] Clone files when copying events
  - [x] badges?
- [x] Actual ticket output
- [x] Default layout on event creation
- [x] Link well from ticketing settings
- [x] Tests
- [x] Shipping plugin
  - [x] Migration
  - [x] Settings
  - [x] Create default
- [x] API
This commit is contained in:
Raphael Michel
2018-06-06 15:27:55 +02:00
committed by GitHub
parent 72661623f3
commit e3450baeb3
29 changed files with 1302 additions and 36 deletions
+110
View File
@@ -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.
+2
View File
@@ -12,3 +12,5 @@ If you want to **create** a plugin, please go to the
list list
pretixdroid pretixdroid
banktransfer banktransfer
ticketoutputpdf
badges
+111
View File
@@ -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.
+1
View File
@@ -43,6 +43,7 @@ inofficial
invalidations invalidations
iterable iterable
Jimdo Jimdo
libpretixprint
libsass libsass
linters linters
memcached memcached
+1 -1
View File
@@ -320,7 +320,7 @@ class CompatibleJSONField(serializers.JSONField):
def to_representation(self, value): def to_representation(self, value):
if value: if value:
return json.load(value) return json.loads(value)
return value return value
+4 -1
View File
@@ -225,8 +225,11 @@ class Renderer:
qr_y = float(o['bottom']) * mm qr_y = float(o['bottom']) * mm
renderPDF.draw(d, canvas, qr_x, qr_y) 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): 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']: if not o['content']:
return '(error)' return '(error)'
if o['content'] == 'other': if o['content'] == 'other':
+30
View File
@@ -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()
+1
View File
@@ -19,6 +19,7 @@ class BadgeItemForm(forms.ModelForm):
event = kwargs.pop('event') event = kwargs.pop('event')
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['layout'].label = _('Badge layout') self.fields['layout'].label = _('Badge layout')
self.fields['layout'].empty_label = _('(Event default)')
self.fields['layout'].queryset = event.badge_layouts.all() self.fields['layout'].queryset = event.badge_layouts.all()
self.fields['layout'].required = False self.fields['layout'].required = False
+4
View File
@@ -70,6 +70,10 @@ def event_copy_data_receiver(sender, other, item_map, **kwargs):
bl.pk = None bl.pk = None
bl.event = sender bl.event = sender
bl.save() bl.save()
if bl.background and bl.background.name:
bl.background.save('background.pdf', bl.background)
layout_map[oldid] = bl layout_map[oldid] = bl
for bi in BadgeItem.objects.filter(item__event=other): for bi in BadgeItem.objects.filter(item__event=other):
+4
View File
@@ -1,5 +1,8 @@
from django.conf.urls import url from django.conf.urls import url
from pretix.api.urls import event_router
from pretix.plugins.badges.api import BadgeLayoutViewSet
from .views import ( from .views import (
LayoutCreate, LayoutDelete, LayoutEditorView, LayoutListView, LayoutCreate, LayoutDelete, LayoutEditorView, LayoutListView,
LayoutSetDefault, OrderPrintDo, LayoutSetDefault, OrderPrintDo,
@@ -19,3 +22,4 @@ urlpatterns = [
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/badges/(?P<layout>\d+)/editor', url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/badges/(?P<layout>\d+)/editor',
LayoutEditorView.as_view(), name='edit'), LayoutEditorView.as_view(), name='edit'),
] ]
event_router.register('badgelayouts', BadgeLayoutViewSet)
+31
View File
@@ -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()
@@ -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()
@@ -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')),
],
),
]
@@ -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)
]
@@ -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')
+95 -12
View File
@@ -1,12 +1,21 @@
import copy
import json
from functools import partial from functools import partial
from django.dispatch import receiver 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 django.utils.translation import ugettext_lazy as _
from pretix.base.models import QuestionAnswer from pretix.base.models import QuestionAnswer
from pretix.base.signals import ( # NOQA: legacy import from pretix.base.signals import ( # NOQA: legacy import
event_copy_data, layout_text_variables, register_data_exporters, event_copy_data, item_copy_data, layout_text_variables, logentry_display,
register_ticket_outputs, 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 from pretix.presale.style import ( # NOQA: legacy import
get_fonts, register_fonts, get_fonts, register_fonts,
@@ -45,14 +54,88 @@ def variables_from_questions(sender, *args, **kwargs):
return d return d
@receiver(signal=event_copy_data, dispatch_uid="pretix_ticketoutputpdf_copy_data") @receiver(item_forms, dispatch_uid="pretix_ticketoutputpdf_item_forms")
def event_copy_data_receiver(sender, other, question_map, **kwargs): def control_item_forms(sender, request, item, **kwargs):
layout = sender.settings.get('ticketoutput_pdf_layout', as_type=list) try:
if not layout: inst = TicketLayoutItem.objects.get(item=item)
return except TicketLayoutItem.DoesNotExist:
for o in layout: inst = TicketLayoutItem(item=item)
if o['type'] == 'textarea': return TicketLayoutItemForm(
if o['content'].startswith('question_'): instance=inst,
o['content'] = 'question_{}'.format(question_map.get(int(o['content'][9:]), 0).pk) 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'] = '<a href="{href}">{val}</a>'.format_map(a_map)
return a_text.format_map(a_map)
@@ -0,0 +1,20 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Ticket layout" %}{% endblock %}
{% block content %}
<h1>{% trans "Ticket layout" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<p>{% blocktrans %}Are you sure you want to delete the layout <strong>{{ layout }}</strong>?{% endblocktrans %}</p>
<div class="form-group submit-group">
<a href="{% url "plugins:badges:index" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn
btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-danger btn-save">
{% trans "Delete" %}
</button>
</div>
</form>
{% endblock %}
@@ -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 %}
<h1>{% blocktrans with name=layout.name %}Ticket layout: {{ name }}{% endblocktrans %}</h1>
{% else %}
<h1>{% trans "Ticket layout" %}</h1>
{% endif %}
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% bootstrap_form_errors form %}
{% bootstrap_field form.name layout="control" %}
<div class="form-group">
<label class="col-md-3 control-label">
{% trans "Ticket design" %}
</label>
<div class="col-md-9">
<p>
{% blocktrans trimmed %}
You can modify the design after you saved this page.
{% endblocktrans %}
</p>
</div>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}
@@ -13,9 +13,13 @@
{% endblocktrans %} {% endblocktrans %}
</p> </p>
<p> <p>
<a class="btn btn-primary" target="_blank"
href="{% url "plugins:ticketoutputpdf:getdefault" organizer=request.organizer.slug event=request.event.slug %}">
{% trans "Change default layout in a new tab" %}
</a>
<a class="btn btn-default" target="_blank" <a class="btn btn-default" target="_blank"
href="{% url "plugins:ticketoutputpdf:editor" organizer=request.organizer.slug event=request.event.slug %}"> href="{% url "plugins:ticketoutputpdf:index" organizer=request.organizer.slug event=request.event.slug %}">
{% trans "Open the PDF editor in a new tab" %} {% trans "Advanced mode (multiple layouts)" %}
</a> </a>
</p> </p>
</div> </div>
@@ -0,0 +1,78 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load money %}
{% block title %}{% trans "Ticket layouts" %}{% endblock %}
{% block content %}
<h1>{% trans "Ticket layouts" %}</h1>
{% if layouts|length == 0 %}
<div class="empty-collection">
<p>
{% blocktrans trimmed %}
You haven't created any layouts yet.
{% endblocktrans %}
</p>
{% if "can_change_event_settings" in request.eventpermset %}
<a href="{% url "plugins:ticketoutputpdf:add" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new layout" %}
</a>
{% endif %}
</div>
{% else %}
<p>
{% if "can_change_event_settings" in request.eventpermset %}
<a href="{% url "plugins:ticketoutputpdf:add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new layout" %}
</a>
{% endif %}
</p>
<div class="table-responsive">
<table class="table table-hover table-quotas">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Default" %}</th>
<th class="action-col-2"></th>
</tr>
</thead>
<tbody>
{% for l in layouts %}
<tr>
<td>
{% if "can_change_event_settings" in request.eventpermset %}
<strong><a href="{% url "plugins:ticketoutputpdf:edit" organizer=request.event.organizer.slug event=request.event.slug layout=l.id %}">
{{ l.name }}
</a></strong>
{% else %}
<strong>{{ l.name }}</strong>
{% endif %}
</td>
<td>
{% if l.default %}
<span class="text-success">
<span class="fa fa-check"></span>
{% trans "Default" %}
</span>
{% elif "can_change_event_settings" in request.eventpermset %}
<form class="form-inline" method="post"
action="{% url "plugins:ticketoutputpdf:default" organizer=request.event.organizer.slug event=request.event.slug layout=l.id %}">
{% csrf_token %}
<button class="btn btn-default btn-sm">
{% trans "Make default" %}
</button>
</form>
{% endif %}
</td>
<td class="text-right">
{% if "can_change_event_settings" in request.eventpermset %}
<a href="{% url "plugins:ticketoutputpdf:edit" organizer=request.event.organizer.slug event=request.event.slug layout=l.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "plugins:ticketoutputpdf:delete" organizer=request.event.organizer.slug event=request.event.slug layout=l.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% include "pretixcontrol/pagination.html" %}
{% endblock %}
@@ -1,18 +1,25 @@
import json
import logging import logging
from io import BytesIO from io import BytesIO
from django.contrib.staticfiles import finders from django.contrib.staticfiles import finders
from django.core.files import File from django.core.files import File
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
from django.http import HttpRequest from django.http import HttpRequest
from django.template.loader import get_template from django.template.loader import get_template
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from PyPDF2 import PdfFileMerger
from reportlab.pdfgen.canvas import Canvas from reportlab.pdfgen.canvas import Canvas
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.models import Order, OrderPosition from pretix.base.models import Order, OrderPosition
from pretix.base.pdf import Renderer from pretix.base.pdf import Renderer
from pretix.base.ticketoutput import BaseTicketOutput from pretix.base.ticketoutput import BaseTicketOutput
from pretix.plugins.ticketoutputpdf.models import (
TicketLayout, TicketLayoutItem,
)
logger = logging.getLogger('pretix.plugins.ticketoutputpdf') logger = logging.getLogger('pretix.plugins.ticketoutputpdf')
@@ -27,35 +34,61 @@ class PdfTicketOutput(BaseTicketOutput):
self.override_background = override_background self.override_background = override_background
super().__init__(event) 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): def _register_fonts(self):
Renderer._register_fonts() Renderer._register_fonts()
def _draw_page(self, canvas: Canvas, op: OrderPosition, order: Order): def _draw_page(self, layout: TicketLayout, canvas: Canvas, op: OrderPosition, order: Order):
objs = self.override_layout or self.settings.get('layout', as_type=list) or self._legacy_layout() objs = self.override_layout or json.loads(layout.layout) or self._legacy_layout()
Renderer(self.event, objs, None).draw_page(canvas, order, op) Renderer(self.event, objs, None).draw_page(canvas, order, op)
def generate_order(self, order: Order): def generate_order(self, order: Order):
buffer = BytesIO() merger = PdfFileMerger()
p = self._create_canvas(buffer)
with language(order.locale): with language(order.locale):
for op in order.positions.all(): for op in order.positions.all():
if op.addon_to_id and not self.event.settings.ticket_download_addons: if op.addon_to_id and not self.event.settings.ticket_download_addons:
continue continue
if not op.item.admission and not self.event.settings.ticket_download_nonadm: if not op.item.admission and not self.event.settings.ticket_download_nonadm:
continue continue
self._draw_page(p, op, order)
p.save() buffer = BytesIO()
outbuffer = self._render_with_background(buffer) 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() return 'order%s%s.pdf' % (self.event.slug, order.code), 'application/pdf', outbuffer.read()
def generate(self, op): def generate(self, op):
buffer = BytesIO() buffer = BytesIO()
p = self._create_canvas(buffer) p = self._create_canvas(buffer)
order = op.order order = op.order
layout = self.layout_map.get(op.item_id, self.default_layout)
with language(order.locale): with language(order.locale):
self._draw_page(p, op, order) self._draw_page(layout, p, op, order)
p.save() 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() return 'order%s%s.pdf' % (self.event.slug, order.code), 'application/pdf', outbuffer.read()
def _create_canvas(self, buffer): def _create_canvas(self, buffer):
@@ -71,11 +104,11 @@ class PdfTicketOutput(BaseTicketOutput):
def _get_default_background(self): def _get_default_background(self):
return open(finders.find('pretixpresale/pdf/ticket_default_a4.pdf'), "rb") return open(finders.find('pretixpresale/pdf/ticket_default_a4.pdf'), "rb")
def _render_with_background(self, buffer, title=_('Ticket')): def _render_with_background(self, layout: TicketLayout, buffer, title=_('Ticket')):
bg_file = self.settings.get('background', as_type=File) bg_file = layout.background
if self.override_background: if self.override_background:
bgf = default_storage.open(self.override_background.name, "rb") 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") bgf = default_storage.open(bg_file.name, "rb")
else: else:
bgf = self._get_default_background() bgf = self._get_default_background()
+19 -3
View File
@@ -1,8 +1,24 @@
from django.conf.urls import url 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 = [ urlpatterns = [
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/pdfoutput/editor/$', views.EditorView.as_view(), url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/pdfoutput/$',
name='editor'), LayoutListView.as_view(), name='index'),
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/pdfoutput/add$',
LayoutCreate.as_view(), name='add'),
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/pdfoutput/(?P<layout>\d+)/default$',
LayoutSetDefault.as_view(), name='default'),
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/pdfoutput/default$',
LayoutGetDefault.as_view(), name='getdefault'),
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/pdfoutput/(?P<layout>\d+)/delete$',
LayoutDelete.as_view(), name='delete'),
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/pdfoutput/(?P<layout>\d+)/editor',
LayoutEditorView.as_view(), name='edit'),
] ]
event_router.register('ticketlayouts', TicketLayoutViewSet)
+211 -2
View File
@@ -1,14 +1,34 @@
import json
import logging 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.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 ( 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.control.views.pdf import BaseEditorView
from pretix.plugins.ticketoutputpdf.forms import TicketLayoutForm
from pretix.plugins.ticketoutputpdf.ticketoutput import PdfTicketOutput from pretix.plugins.ticketoutputpdf.ticketoutput import PdfTicketOutput
from .models import TicketLayout
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -50,3 +70,192 @@ class EditorView(BaseEditorView):
self.request.event.settings.get(self.get_layout_settings_key(), as_type=list) self.request.event.settings.get(self.get_layout_settings_key(), as_type=list)
or prov._default_layout() 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()
+61
View File
@@ -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
@@ -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
@@ -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)
@@ -13,7 +13,7 @@ from pretix.plugins.ticketoutputpdf.ticketoutput import PdfTicketOutput
@pytest.fixture @pytest.fixture
def env(): def env0():
o = Organizer.objects.create(name='Dummy', slug='dummy') o = Organizer.objects.create(name='Dummy', slug='dummy')
event = Event.objects.create( event = Event.objects.create(
organizer=o, name='Dummy', slug='dummy', organizer=o, name='Dummy', slug='dummy',
@@ -39,8 +39,8 @@ def env():
@pytest.mark.django_db @pytest.mark.django_db
def test_generate_pdf(env): def test_generate_pdf(env0):
event, order = env event, order = env0
event.settings.set('ticketoutput_pdf_code_x', 30) event.settings.set('ticketoutput_pdf_code_x', 30)
event.settings.set('ticketoutput_pdf_code_y', 50) event.settings.set('ticketoutput_pdf_code_y', 50)
event.settings.set('ticketoutput_pdf_code_s', 2) event.settings.set('ticketoutput_pdf_code_s', 2)