mirror of
https://github.com/pretix/pretix.git
synced 2025-12-11 01:22:28 +00:00
Compare commits
4 Commits
widget-dat
...
pdf-layout
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1263f4359f | ||
|
|
645df63e66 | ||
|
|
3416366763 | ||
|
|
bbefb59036 |
@@ -29,8 +29,8 @@ item_assignments list of objects Products this l
|
|||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|
||||||
Endpoints
|
Layout endpoints
|
||||||
---------
|
----------------
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/ticketlayouts/
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/ticketlayouts/
|
||||||
|
|
||||||
@@ -268,5 +268,75 @@ Endpoints
|
|||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
|
||||||
|
|
||||||
|
Ticket rendering endpoint
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/ticketpdfrenderer/render_batch/
|
||||||
|
|
||||||
|
With this API call, you can instruct the system to render a set of tickets into one combined PDF file. To specify
|
||||||
|
which tickets to render, you need to submit a list of "parts". For every part, the following fields are supported:
|
||||||
|
|
||||||
|
* ``orderposition`` (``integer``, required): The ID of the order position to render.
|
||||||
|
* ``override_channel`` (``string``, optional): The sales channel ID to be used for layout selection instead of the
|
||||||
|
original channel of the order.
|
||||||
|
* ``override_layout`` (``integer``, optional): The ticket layout ID to be used instead of the auto-selected one.
|
||||||
|
|
||||||
|
If your input parameters validate correctly, a ``202 Accepted`` status code is returned.
|
||||||
|
The body points you to the download URL of the result. Running a ``GET`` request on that result URL will
|
||||||
|
yield one of the following status codes:
|
||||||
|
|
||||||
|
* ``200 OK`` – The export succeeded. The body will be your resulting file. Might be large!
|
||||||
|
* ``409 Conflict`` – Your export is still running. The body will be JSON with the structure ``{"status": "running"}``. ``status`` can be ``waiting`` before the task is actually being processed. Please retry, but wait at least one second before you do.
|
||||||
|
* ``410 Gone`` – Running the export has failed permanently. The body will be JSON with the structure ``{"status": "failed", "message": "Error message"}``
|
||||||
|
* ``404 Not Found`` – The export does not exist / is expired.
|
||||||
|
|
||||||
|
.. warning:: This endpoint is considered **experimental**. It might change at any time without prior notice.
|
||||||
|
|
||||||
|
.. note:: To avoid performance issues, a maximum number of 1000 parts is currently allowed.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/ticketpdfrenderer/render_batch/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"parts": [
|
||||||
|
{
|
||||||
|
"orderposition": 55412
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"orderposition": 55412,
|
||||||
|
"override_channel": "web"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"orderposition": 55412,
|
||||||
|
"override_layout": 56
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"download": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/ticketpdfrenderer/download/29891ede-196f-4942-9e26-d055a36e98b8/3f279f13-c198-4137-b49b-9b360ce9fcce/"
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
:statuscode 202: no error
|
||||||
|
:statuscode 400: Invalid input options
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||||
|
|
||||||
|
|
||||||
.. _here: https://github.com/pretix/pretix/blob/master/src/pretix/static/schema/pdf-layout.schema.json
|
.. _here: https://github.com/pretix/pretix/blob/master/src/pretix/static/schema/pdf-layout.schema.json
|
||||||
|
|||||||
@@ -19,21 +19,33 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from celery.result import AsyncResult
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
|
from django.http import Http404
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.functional import lazy
|
from django.utils.functional import lazy
|
||||||
from rest_framework import serializers, viewsets
|
from django.utils.timezone import now
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
from rest_framework import serializers, status, viewsets
|
||||||
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.reverse import reverse
|
||||||
|
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.api.serializers.order import CompatibleJSONField
|
from pretix.api.serializers.order import CompatibleJSONField
|
||||||
|
|
||||||
from ...api.serializers.fields import UploadedFileField
|
from ...api.serializers.fields import UploadedFileField
|
||||||
from ...base.models import SalesChannel
|
from ...base.models import CachedFile, OrderPosition, SalesChannel
|
||||||
from ...base.pdf import PdfLayoutValidator
|
from ...base.pdf import PdfLayoutValidator
|
||||||
|
from ...helpers.http import ChunkBasedFileResponse
|
||||||
from ...multidomain.utils import static_absolute
|
from ...multidomain.utils import static_absolute
|
||||||
from .models import TicketLayout, TicketLayoutItem
|
from .models import TicketLayout, TicketLayoutItem
|
||||||
|
from .tasks import bulk_render
|
||||||
|
|
||||||
|
|
||||||
class ItemAssignmentSerializer(I18nAwareModelSerializer):
|
class ItemAssignmentSerializer(I18nAwareModelSerializer):
|
||||||
@@ -156,3 +168,123 @@ class TicketLayoutItemViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
**super().get_serializer_context(),
|
**super().get_serializer_context(),
|
||||||
'event': self.request.event,
|
'event': self.request.event,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
with scopes_disabled():
|
||||||
|
class RenderJobPartSerializer(serializers.Serializer):
|
||||||
|
orderposition = serializers.PrimaryKeyRelatedField(
|
||||||
|
queryset=OrderPosition.objects.none(),
|
||||||
|
required=True,
|
||||||
|
allow_null=False,
|
||||||
|
)
|
||||||
|
override_layout = serializers.PrimaryKeyRelatedField(
|
||||||
|
queryset=TicketLayout.objects.none(),
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
)
|
||||||
|
override_channel = serializers.SlugRelatedField(
|
||||||
|
queryset=SalesChannel.objects.none(),
|
||||||
|
slug_field='identifier',
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RenderJobSerializer(serializers.Serializer):
|
||||||
|
parts = RenderJobPartSerializer(many=True)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['parts'].child.fields['orderposition'].queryset = OrderPosition.objects.filter(order__event=self.context['event'])
|
||||||
|
self.fields['parts'].child.fields['override_layout'].queryset = self.context['event'].ticket_layouts.all()
|
||||||
|
self.fields['parts'].child.fields['override_channel'].queryset = self.context['event'].organizer.sales_channels.all()
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
if len(attrs["parts"]) > 1000:
|
||||||
|
raise ValidationError({"parts": ["Please do not submit more than 1000 parts."]})
|
||||||
|
return super().validate(attrs)
|
||||||
|
|
||||||
|
|
||||||
|
class TicketRendererViewSet(viewsets.ViewSet):
|
||||||
|
permission = 'can_view_orders'
|
||||||
|
|
||||||
|
def get_serializer_kwargs(self):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
raise Http404()
|
||||||
|
|
||||||
|
def retrieve(self, request, *args, **kwargs):
|
||||||
|
raise Http404()
|
||||||
|
|
||||||
|
def update(self, request, *args, **kwargs):
|
||||||
|
raise Http404()
|
||||||
|
|
||||||
|
def partial_update(self, request, *args, **kwargs):
|
||||||
|
raise Http404()
|
||||||
|
|
||||||
|
def destroy(self, request, *args, **kwargs):
|
||||||
|
raise Http404()
|
||||||
|
|
||||||
|
@action(detail=False, methods=['GET'], url_name='download', url_path='download/(?P<asyncid>[^/]+)/(?P<cfid>[^/]+)')
|
||||||
|
def download(self, *args, **kwargs):
|
||||||
|
cf = get_object_or_404(CachedFile, id=kwargs['cfid'])
|
||||||
|
if cf.file:
|
||||||
|
resp = ChunkBasedFileResponse(cf.file.file, content_type=cf.type)
|
||||||
|
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename).encode("ascii", "ignore")
|
||||||
|
return resp
|
||||||
|
elif not settings.HAS_CELERY:
|
||||||
|
return Response(
|
||||||
|
{'status': 'failed', 'message': 'Unknown file ID or export failed'},
|
||||||
|
status=status.HTTP_410_GONE
|
||||||
|
)
|
||||||
|
|
||||||
|
res = AsyncResult(kwargs['asyncid'])
|
||||||
|
if res.failed():
|
||||||
|
if isinstance(res.info, dict) and res.info['exc_type'] == 'ExportError':
|
||||||
|
msg = res.info['exc_message']
|
||||||
|
else:
|
||||||
|
msg = 'Internal error'
|
||||||
|
return Response(
|
||||||
|
{'status': 'failed', 'message': msg},
|
||||||
|
status=status.HTTP_410_GONE
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
'status': 'running' if res.state in ('PROGRESS', 'STARTED', 'SUCCESS') else 'waiting',
|
||||||
|
},
|
||||||
|
status=status.HTTP_409_CONFLICT
|
||||||
|
)
|
||||||
|
|
||||||
|
@action(detail=False, methods=['POST'])
|
||||||
|
def render_batch(self, *args, **kwargs):
|
||||||
|
serializer = RenderJobSerializer(data=self.request.data, context={
|
||||||
|
"event": self.request.event,
|
||||||
|
})
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
cf = CachedFile(web_download=False)
|
||||||
|
cf.date = now()
|
||||||
|
cf.expires = now() + timedelta(hours=24)
|
||||||
|
cf.save()
|
||||||
|
async_result = bulk_render.apply_async(args=(
|
||||||
|
self.request.event.id,
|
||||||
|
str(cf.id),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"orderposition": r["orderposition"].id,
|
||||||
|
"override_layout": r["override_layout"].id if r.get("override_layout") else None,
|
||||||
|
"override_channel": r["override_channel"].id if r.get("override_channel") else None,
|
||||||
|
} for r in serializer.validated_data["parts"]
|
||||||
|
]
|
||||||
|
))
|
||||||
|
|
||||||
|
url_kwargs = {
|
||||||
|
'asyncid': str(async_result.id),
|
||||||
|
'cfid': str(cf.id),
|
||||||
|
}
|
||||||
|
url_kwargs.update(self.kwargs)
|
||||||
|
return Response({
|
||||||
|
'download': reverse('api-v1:ticketpdfrenderer-download', kwargs=url_kwargs, request=self.request)
|
||||||
|
}, status=status.HTTP_202_ACCEPTED)
|
||||||
|
|||||||
@@ -19,18 +19,26 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
|
from django.db.models import Prefetch, prefetch_related_objects
|
||||||
|
from pypdf import PdfWriter
|
||||||
|
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CachedFile, Event, OrderPosition, cachedfile_name,
|
CachedFile, Checkin, Event, EventMetaValue, ItemMetaValue,
|
||||||
|
ItemVariationMetaValue, OrderPosition, SalesChannel, SubEventMetaValue,
|
||||||
|
cachedfile_name,
|
||||||
)
|
)
|
||||||
from pretix.base.services.orders import OrderError
|
from pretix.base.services.orders import OrderError
|
||||||
from pretix.base.services.tasks import EventTask
|
from pretix.base.services.tasks import EventTask
|
||||||
from pretix.celery_app import app
|
from pretix.celery_app import app
|
||||||
|
|
||||||
|
from ...base.i18n import language
|
||||||
from ...base.services.export import ExportError
|
from ...base.services.export import ExportError
|
||||||
|
from .models import TicketLayout
|
||||||
from .ticketoutput import PdfTicketOutput
|
from .ticketoutput import PdfTicketOutput
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -46,3 +54,93 @@ def tickets_create_pdf(event: Event, fileid: int, position: int, channel) -> int
|
|||||||
file.file.save(cachedfile_name(file, file.filename), ContentFile(data))
|
file.file.save(cachedfile_name(file, file.filename), ContentFile(data))
|
||||||
file.save()
|
file.save()
|
||||||
return file.pk
|
return file.pk
|
||||||
|
|
||||||
|
|
||||||
|
@app.task(base=EventTask, throws=(OrderError, ExportError,))
|
||||||
|
def bulk_render(event: Event, fileid: int, parts: list) -> int:
|
||||||
|
file = CachedFile.objects.get(id=fileid)
|
||||||
|
|
||||||
|
channels = SalesChannel.objects.in_bulk([p["override_channel"] for p in parts if p.get("override_channel")])
|
||||||
|
layouts = TicketLayout.objects.in_bulk([p["override_layout"] for p in parts if p.get("override_layout")])
|
||||||
|
|
||||||
|
positions = OrderPosition.objects.all()
|
||||||
|
prefetch_related_objects([event.organizer], 'meta_properties')
|
||||||
|
prefetch_related_objects(
|
||||||
|
[event],
|
||||||
|
Prefetch('meta_values', queryset=EventMetaValue.objects.select_related('property'),
|
||||||
|
to_attr='meta_values_cached'),
|
||||||
|
'questions',
|
||||||
|
'item_meta_properties',
|
||||||
|
)
|
||||||
|
positions = positions.prefetch_related(
|
||||||
|
Prefetch('checkins', queryset=Checkin.objects.select_related("device")),
|
||||||
|
Prefetch('item', queryset=event.items.prefetch_related(
|
||||||
|
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'),
|
||||||
|
to_attr='meta_values_cached')
|
||||||
|
)),
|
||||||
|
'variation',
|
||||||
|
'answers', 'answers__options', 'answers__question',
|
||||||
|
'item__category',
|
||||||
|
'addon_to__answers', 'addon_to__answers__options', 'addon_to__answers__question',
|
||||||
|
Prefetch('addons', positions.select_related('item', 'variation')),
|
||||||
|
Prefetch('subevent', queryset=event.subevents.prefetch_related(
|
||||||
|
Prefetch('meta_values', to_attr='meta_values_cached',
|
||||||
|
queryset=SubEventMetaValue.objects.select_related('property'))
|
||||||
|
)),
|
||||||
|
'linked_media',
|
||||||
|
Prefetch('order', event.orders.select_related('invoice_address').prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
'positions',
|
||||||
|
positions.prefetch_related(
|
||||||
|
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
||||||
|
Prefetch('item', queryset=event.items.prefetch_related(
|
||||||
|
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'),
|
||||||
|
to_attr='meta_values_cached')
|
||||||
|
)),
|
||||||
|
Prefetch('variation', queryset=event.items.prefetch_related(
|
||||||
|
Prefetch('meta_values', ItemVariationMetaValue.objects.select_related('property'),
|
||||||
|
to_attr='meta_values_cached')
|
||||||
|
)),
|
||||||
|
'answers', 'answers__options', 'answers__question',
|
||||||
|
'item__category',
|
||||||
|
Prefetch('subevent', queryset=event.subevents.prefetch_related(
|
||||||
|
Prefetch('meta_values', to_attr='meta_values_cached',
|
||||||
|
queryset=SubEventMetaValue.objects.select_related('property'))
|
||||||
|
)),
|
||||||
|
Prefetch('addons', positions.select_related('item', 'variation', 'seat'))
|
||||||
|
).select_related('addon_to', 'seat', 'addon_to__seat')
|
||||||
|
)
|
||||||
|
))
|
||||||
|
).select_related(
|
||||||
|
'addon_to', 'seat', 'addon_to__seat'
|
||||||
|
)
|
||||||
|
positions = positions.in_bulk([p["orderposition"] for p in parts])
|
||||||
|
|
||||||
|
merger = PdfWriter()
|
||||||
|
for part in parts:
|
||||||
|
p = positions[part["orderposition"]]
|
||||||
|
p.order.event = event # performance optimization
|
||||||
|
with (language(p.order.locale)):
|
||||||
|
kwargs = {}
|
||||||
|
if part.get("override_channel"):
|
||||||
|
kwargs["override_channel"] = channels[part["override_channel"]].identifier
|
||||||
|
if part.get("override_layout"):
|
||||||
|
l = layouts[part["override_layout"]]
|
||||||
|
kwargs["override_layout"] = json.loads(l.layout)
|
||||||
|
kwargs["override_background"] = l.background
|
||||||
|
prov = PdfTicketOutput(
|
||||||
|
event,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
filename, ctype, data = prov.generate(p)
|
||||||
|
merger.append(ContentFile(data))
|
||||||
|
|
||||||
|
outbuffer = BytesIO()
|
||||||
|
merger.write(outbuffer)
|
||||||
|
merger.close()
|
||||||
|
outbuffer.seek(0)
|
||||||
|
|
||||||
|
file.type = "application/pdf"
|
||||||
|
file.file.save(cachedfile_name(file, file.filename), ContentFile(outbuffer.getvalue()))
|
||||||
|
file.save()
|
||||||
|
return file.pk
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from django.urls import re_path
|
|||||||
|
|
||||||
from pretix.api.urls import event_router
|
from pretix.api.urls import event_router
|
||||||
from pretix.plugins.ticketoutputpdf.api import (
|
from pretix.plugins.ticketoutputpdf.api import (
|
||||||
TicketLayoutItemViewSet, TicketLayoutViewSet,
|
TicketLayoutItemViewSet, TicketLayoutViewSet, TicketRendererViewSet,
|
||||||
)
|
)
|
||||||
from pretix.plugins.ticketoutputpdf.views import (
|
from pretix.plugins.ticketoutputpdf.views import (
|
||||||
LayoutCreate, LayoutDelete, LayoutEditorView, LayoutGetDefault,
|
LayoutCreate, LayoutDelete, LayoutEditorView, LayoutGetDefault,
|
||||||
@@ -48,3 +48,4 @@ urlpatterns = [
|
|||||||
]
|
]
|
||||||
event_router.register('ticketlayouts', TicketLayoutViewSet)
|
event_router.register('ticketlayouts', TicketLayoutViewSet)
|
||||||
event_router.register('ticketlayoutitems', TicketLayoutItemViewSet)
|
event_router.register('ticketlayoutitems', TicketLayoutItemViewSet)
|
||||||
|
event_router.register('ticketpdfrenderer', TicketRendererViewSet, basename='ticketpdfrenderer')
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
#
|
#
|
||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
|
from datetime import timedelta
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
@@ -28,7 +30,9 @@ from django.utils.timezone import now
|
|||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from pretix.base.models import Event, Item, Organizer, Team
|
from pretix.base.models import (
|
||||||
|
Event, Item, Order, OrderPosition, Organizer, Team,
|
||||||
|
)
|
||||||
from pretix.plugins.ticketoutputpdf.models import TicketLayoutItem
|
from pretix.plugins.ticketoutputpdf.models import TicketLayoutItem
|
||||||
|
|
||||||
|
|
||||||
@@ -39,14 +43,35 @@ def env():
|
|||||||
organizer=o, name='Dummy', slug='dummy',
|
organizer=o, name='Dummy', slug='dummy',
|
||||||
date_from=now(), plugins='pretix.plugins.banktransfer'
|
date_from=now(), plugins='pretix.plugins.banktransfer'
|
||||||
)
|
)
|
||||||
t = Team.objects.create(organizer=event.organizer)
|
t = Team.objects.create(organizer=event.organizer, can_view_orders=True)
|
||||||
t.limit_events.add(event)
|
t.limit_events.add(event)
|
||||||
item1 = Item.objects.create(event=event, name="Ticket", default_price=23)
|
item1 = Item.objects.create(event=event, name="Ticket", default_price=23)
|
||||||
tl = event.ticket_layouts.create(name="Foo", default=True, layout='[{"a": 2}]')
|
tl = event.ticket_layouts.create(
|
||||||
|
name="Foo",
|
||||||
|
default=True,
|
||||||
|
layout='[{"type": "poweredby", "left": "0", "bottom": "0", "size": "1.00", "content": "dark"}]',
|
||||||
|
)
|
||||||
TicketLayoutItem.objects.create(layout=tl, item=item1, sales_channel=o.sales_channels.get(identifier="web"))
|
TicketLayoutItem.objects.create(layout=tl, item=item1, sales_channel=o.sales_channels.get(identifier="web"))
|
||||||
return event, tl, item1
|
return event, tl, item1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def position(env):
|
||||||
|
item = env[0].items.create(name="Ticket", default_price=3, admission=True)
|
||||||
|
order = Order.objects.create(
|
||||||
|
code='FOO', event=env[0], email='dummy@dummy.test',
|
||||||
|
status=Order.STATUS_PAID, locale='en',
|
||||||
|
datetime=now() - timedelta(days=4),
|
||||||
|
expires=now() - timedelta(hours=4) + timedelta(days=10),
|
||||||
|
total=Decimal('23.00'),
|
||||||
|
sales_channel=env[0].organizer.sales_channels.get(identifier="web"),
|
||||||
|
)
|
||||||
|
return OrderPosition.objects.create(
|
||||||
|
order=order, item=item, variation=None,
|
||||||
|
price=Decimal("23.00"), attendee_name_parts={"full_name": "Peter"}, positionid=1
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def client():
|
def client():
|
||||||
return APIClient()
|
return APIClient()
|
||||||
@@ -65,7 +90,7 @@ RES_LAYOUT = {
|
|||||||
'name': 'Foo',
|
'name': 'Foo',
|
||||||
'default': True,
|
'default': True,
|
||||||
'item_assignments': [{'item': 1, 'sales_channel': 'web'}],
|
'item_assignments': [{'item': 1, 'sales_channel': 'web'}],
|
||||||
'layout': [{'a': 2}],
|
'layout': [{"type": "poweredby", "left": "0", "bottom": "0", "size": "1.00", "content": "dark"}],
|
||||||
'background': 'http://example.com/static/pretixpresale/pdf/ticket_default_a4.pdf'
|
'background': 'http://example.com/static/pretixpresale/pdf/ticket_default_a4.pdf'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,3 +238,92 @@ def test_api_delete(env, token_client):
|
|||||||
)
|
)
|
||||||
assert resp.status_code == 204
|
assert resp.status_code == 204
|
||||||
assert not env[0].ticket_layouts.exists()
|
assert not env[0].ticket_layouts.exists()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_renderer_batch_valid(env, token_client, position):
|
||||||
|
resp = token_client.post(
|
||||||
|
'/api/v1/organizers/{}/events/{}/ticketpdfrenderer/render_batch/'.format(env[0].slug, env[0].slug),
|
||||||
|
{
|
||||||
|
"parts": [
|
||||||
|
{
|
||||||
|
"orderposition": position.pk,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"orderposition": position.pk,
|
||||||
|
"override_channel": "web",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"orderposition": position.pk,
|
||||||
|
"override_layout": env[1].pk,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
format='json',
|
||||||
|
)
|
||||||
|
assert resp.status_code == 202
|
||||||
|
assert "download" in resp.data
|
||||||
|
resp = token_client.get("/" + resp.data["download"].split("/", 3)[3])
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp["Content-Type"] == "application/pdf"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_renderer_batch_invalid(env, token_client, position):
|
||||||
|
resp = token_client.post(
|
||||||
|
'/api/v1/organizers/{}/events/{}/ticketpdfrenderer/render_batch/'.format(env[0].slug, env[0].slug),
|
||||||
|
{
|
||||||
|
"parts": [
|
||||||
|
{
|
||||||
|
"orderposition": -2,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
format='json',
|
||||||
|
)
|
||||||
|
assert resp.status_code == 400
|
||||||
|
assert resp.data == {"parts": [{"orderposition": ["Invalid pk \"-2\" - object does not exist."]}]}
|
||||||
|
|
||||||
|
resp = token_client.post(
|
||||||
|
'/api/v1/organizers/{}/events/{}/ticketpdfrenderer/render_batch/'.format(env[0].slug, env[0].slug),
|
||||||
|
{
|
||||||
|
"parts": [
|
||||||
|
{
|
||||||
|
"orderposition": position.pk,
|
||||||
|
"override_layout": -2,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
format='json',
|
||||||
|
)
|
||||||
|
assert resp.status_code == 400
|
||||||
|
assert resp.data == {"parts": [{"override_layout": ["Invalid pk \"-2\" - object does not exist."]}]}
|
||||||
|
|
||||||
|
resp = token_client.post(
|
||||||
|
'/api/v1/organizers/{}/events/{}/ticketpdfrenderer/render_batch/'.format(env[0].slug, env[0].slug),
|
||||||
|
{
|
||||||
|
"parts": [
|
||||||
|
{
|
||||||
|
"orderposition": position.pk,
|
||||||
|
"override_channel": "magic",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
format='json',
|
||||||
|
)
|
||||||
|
assert resp.status_code == 400
|
||||||
|
assert resp.data == {"parts": [{"override_channel": ["Object with identifier=magic does not exist."]}]}
|
||||||
|
|
||||||
|
resp = token_client.post(
|
||||||
|
'/api/v1/organizers/{}/events/{}/ticketpdfrenderer/render_batch/'.format(env[0].slug, env[0].slug),
|
||||||
|
{
|
||||||
|
"parts": [
|
||||||
|
{
|
||||||
|
"orderposition": position.pk,
|
||||||
|
}
|
||||||
|
] * 1002
|
||||||
|
},
|
||||||
|
format='json',
|
||||||
|
)
|
||||||
|
assert resp.status_code == 400
|
||||||
|
assert resp.data == {"parts": ["Please do not submit more than 1000 parts."]}
|
||||||
|
|||||||
Reference in New Issue
Block a user