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" %}

+
+ {% csrf_token %} +

{% blocktrans %}Are you sure you want to delete the layout {{ layout }}?{% endblocktrans %}

+
+ + {% trans "Cancel" %} + + +
+
+{% 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 %} +
+ {% csrf_token %} + {% bootstrap_form_errors form %} + {% bootstrap_field form.name layout="control" %} +
+ +
+

+ {% blocktrans trimmed %} + You can modify the design after you saved this page. + {% endblocktrans %} +

+
+
+
+ +
+
+{% 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 %} +

+
+ + + + + + + + + + {% for l in layouts %} + + + + + + {% endfor %} + +
{% trans "Name" %}{% trans "Default" %}
+ {% 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 %} +
+ {% csrf_token %} + +
+ {% endif %} +
+ {% if "can_change_event_settings" in request.eventpermset %} + + + {% endif %} +
+
+ {% 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)