diff --git a/doc/api/resources/orders.rst b/doc/api/resources/orders.rst index 4aaf3e18b5..a4de204642 100644 --- a/doc/api/resources/orders.rst +++ b/doc/api/resources/orders.rst @@ -219,6 +219,11 @@ pdf_data object Data object req The value ``auto_checked_in`` has been added to the ``checkins``-attribute. +.. versionchanged:: 3.3 + + The ``url`` of a ticket ``download`` can now also return a ``text/uri-list`` instead of a file. See + :ref:`order-position-ticket-download` for details. + .. _order-payment-resource: Order payment resource @@ -1483,6 +1488,8 @@ Fetching individual positions :statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource. :statuscode 404: The requested order position does not exist. +.. _`order-position-ticket-download`: + Order position ticket download ------------------------------ @@ -1492,6 +1499,11 @@ Order position ticket download Depending on the chosen output, the response might be a ZIP file, PDF file or something else. The order details response contains a list of output options for this particular order position. + Be aware that the output does not have to be a file, but can also be a regular HTTP response with a ``Content-Type`` + set to ``text/uri-list``. In this case, the user is expected to navigate to that URL in order to access their ticket. + The referenced URL can provide a download or a regular, human-viewable website - so it is advised to open this URL + in a webbrowser and leave it up to the user to handle the result. + Tickets can be only downloaded if the order is paid and if ticket downloads are active. Also, depending on event configuration downloads might be only unavailable for add-on products or non-admission products. Note that in some cases the ticket file might not yet have been created. In that case, you will receive a status diff --git a/doc/development/api/ticketoutput.rst b/doc/development/api/ticketoutput.rst index a9cfdbe1ad..ac1ca64729 100644 --- a/doc/development/api/ticketoutput.rst +++ b/doc/development/api/ticketoutput.rst @@ -69,3 +69,9 @@ The output class .. automethod:: generate_order .. autoattribute:: download_button_text + + .. autoattribute:: download_button_icon + + .. autoattribute:: preview_allowed + + .. autoattribute:: javascript_required diff --git a/doc/spelling_wordlist.txt b/doc/spelling_wordlist.txt index 6ddbea01e4..04b53d0329 100644 --- a/doc/spelling_wordlist.txt +++ b/doc/spelling_wordlist.txt @@ -31,6 +31,7 @@ deprovision discoverable django dockerfile +downloadable durations eu filename @@ -140,6 +141,7 @@ username url versa versioning +viewable viewset viewsets waitinglist diff --git a/src/pretix/api/views/order.py b/src/pretix/api/views/order.py index 3545f8b232..d9d9ac5f70 100644 --- a/src/pretix/api/views/order.py +++ b/src/pretix/api/views/order.py @@ -6,7 +6,7 @@ import pytz from django.db import transaction from django.db.models import F, Prefetch, Q from django.db.models.functions import Coalesce, Concat -from django.http import FileResponse +from django.http import FileResponse, HttpResponse from django.shortcuts import get_object_or_404 from django.utils.timezone import make_aware, now from django.utils.translation import ugettext as _ @@ -148,12 +148,16 @@ class OrderViewSet(viewsets.ModelViewSet): generate.apply_async(args=('order', order.pk, provider.identifier)) raise RetryException() else: - resp = FileResponse(ct.file.file, content_type=ct.type) - resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format( - self.request.event.slug.upper(), order.code, - provider.identifier, ct.extension - ) - return resp + if ct.type == 'text/uri-list': + resp = HttpResponse(ct.file.file.read(), content_type='text/uri-list') + return resp + else: + resp = FileResponse(ct.file.file, content_type=ct.type) + resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format( + self.request.event.slug.upper(), order.code, + provider.identifier, ct.extension + ) + return resp @action(detail=True, methods=['POST']) def mark_paid(self, request, **kwargs): @@ -759,12 +763,16 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS generate.apply_async(args=('orderposition', pos.pk, provider.identifier)) raise RetryException() else: - resp = FileResponse(ct.file.file, content_type=ct.type) - resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format( - self.request.event.slug.upper(), pos.order.code, pos.positionid, - provider.identifier, ct.extension - ) - return resp + if ct.type == 'text/uri-list': + resp = HttpResponse(ct.file.file.read(), content_type='text/uri-list') + return resp + else: + resp = FileResponse(ct.file.file, content_type=ct.type) + resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format( + self.request.event.slug.upper(), pos.order.code, pos.positionid, + provider.identifier, ct.extension + ) + return resp def perform_destroy(self, instance): try: diff --git a/src/pretix/base/services/tickets.py b/src/pretix/base/services/tickets.py index bb327888dc..06491194c5 100644 --- a/src/pretix/base/services/tickets.py +++ b/src/pretix/base/services/tickets.py @@ -47,6 +47,9 @@ def generate_order(order: int, provider: str): prov = response(order.event) if prov.identifier == provider: filename, ttype, data = prov.generate_order(order) + if ttype == 'text/uri-list': + continue + path, ext = os.path.splitext(filename) for ct in CachedCombinedTicket.objects.filter(order=order, provider=provider): ct.delete() @@ -124,6 +127,9 @@ def get_tickets_for_order(order, base_position=None): if not p.is_enabled: continue + if p.download_handled_by_frontend: + continue + if p.multi_download_enabled and not base_position: try: if len(positions) == 0: diff --git a/src/pretix/base/ticketoutput.py b/src/pretix/base/ticketoutput.py index d569862fd3..16aa6bc412 100644 --- a/src/pretix/base/ticketoutput.py +++ b/src/pretix/base/ticketoutput.py @@ -46,12 +46,19 @@ class BaseTicketOutput: filename, a file type and file content. The extension will be taken from the filename which is otherwise ignored. + Alternatively, you can pass a tuple consisting of an arbitrary string, ``text/uri-list`` + and a single URL. In this case, the user will be redirected to this link instead of + being asked to download a generated file. + .. note:: If the event uses the event series feature (internally called subevents) and your generated ticket contains information like the event name or date, you probably want to display the properties of the subevent. A common pattern to do this would be a declaration ``ev = position.subevent or position.order.event`` and then access properties that are present on both classes like ``ev.name`` or ``ev.date_from``. + + .. note:: Should you elect to use the URI redirection feature instead of offering downloads, + you should also set the ``multi_download_enabled``-property to ``False``. """ raise NotImplementedError() @@ -161,3 +168,21 @@ class BaseTicketOutput: The Font Awesome icon on the download button in the frontend. """ return 'fa-download' + + @property + def preview_allowed(self) -> bool: + """ + By default, the ``generate()`` method is called for generating a preview in the pretix backend. + In case your plugin cannot generate previews for any reason, you can manually disable it here. + """ + return True + + @property + def javascript_required(self) -> bool: + """ + If this property is set to true, the download-button for this ticket-type will not be displayed + when the user's browser has JavaScript disabled. + + Defaults to ``False`` + """ + return False diff --git a/src/pretix/control/templates/pretixcontrol/event/tickets.html b/src/pretix/control/templates/pretixcontrol/event/tickets.html index 18904955ca..65e8e4b216 100644 --- a/src/pretix/control/templates/pretixcontrol/event/tickets.html +++ b/src/pretix/control/templates/pretixcontrol/event/tickets.html @@ -29,7 +29,7 @@