Allow ticket output providers to handle downloads externally (#1402)

* TicketOutput-Providers: Make preview optional; download/attachable optional; optional specific target; update doc

* Spelling fixes in doc

* Changes after code-review

* Changes after code-review

* Commit missing template file

* Allow for redirects instead of files

* Return HTTPResponse with Content-Type text/uri-list on API

* Update API-doc

* Add viewable to spellinglist, fixing doc-test
This commit is contained in:
Martin Gross
2019-10-21 14:05:09 +02:00
committed by Raphael Michel
parent 27538d220e
commit 03c760c2bb
14 changed files with 123 additions and 39 deletions

View File

@@ -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:

View File

@@ -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:

View File

@@ -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

View File

@@ -29,7 +29,7 @@
<div class="panel panel-default ticketoutput-panel">
<div class="panel-heading">
<a href="{% url "control:event.settings.tickets.preview" event=request.event.slug organizer=request.organizer.slug output=provider.identifier %}"
class="btn btn-default btn-sm pull-right flip {% if not provider.preview_allowed %}disabled{% endif %}"
class="btn btn-default btn-sm pull-right flip {% if not provider.evaluated_preview_allowed %}disabled{% endif %}"
target="_blank">
{% trans "Preview" %}
</a>

View File

@@ -287,7 +287,7 @@
{% for b in download_buttons %}
<form action="{% url "control:event.order.download.ticket" code=order.code event=request.event.slug organizer=request.event.organizer.slug position=line.pk output=b.identifier %}"
method="post" data-asynctask data-asynctask-download
class="form-inline helper-display-inline">
class="form-inline helper-display-inline{% if b.javascript_required %} requirejs{% endif %}">
{% csrf_token %}
<button type="submit"
class="btn btn-xs btn-default">

View File

@@ -736,11 +736,14 @@ class TicketSettings(EventSettingsViewMixin, EventPermissionRequiredMixin, FormV
provider.settings_content = provider.settings_content_render(self.request)
provider.form.prepare_fields()
provider.preview_allowed = True
for k, v in provider.settings_form_fields.items():
if v.required and not self.request.event.settings.get('ticketoutput_%s_%s' % (provider.identifier, k)):
provider.preview_allowed = False
break
provider.evaluated_preview_allowed = True
if not provider.preview_allowed:
provider.evaluated_preview_allowed = False
else:
for k, v in provider.settings_form_fields.items():
if v.required and not self.request.event.settings.get('ticketoutput_%s_%s' % (provider.identifier, k)):
provider.evaluated_preview_allowed = False
break
providers.append(provider)
return providers

View File

@@ -16,7 +16,8 @@ from django.db.models import (
)
from django.forms import formset_factory
from django.http import (
FileResponse, Http404, HttpResponseNotAllowed, JsonResponse,
FileResponse, Http404, HttpResponseNotAllowed, HttpResponseRedirect,
JsonResponse,
)
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
@@ -225,7 +226,8 @@ class OrderDetail(OrderView):
'text': provider.download_button_text or 'Ticket',
'icon': provider.download_button_icon or 'fa-download',
'identifier': provider.identifier,
'multi': provider.multi_download_enabled
'multi': provider.multi_download_enabled,
'javascript_required': provider.javascript_required
})
return buttons
@@ -340,12 +342,16 @@ class OrderDownload(AsyncAction, OrderView):
'message': str(self.get_success_message(value))
})
if isinstance(value, CachedTicket):
resp = FileResponse(value.file.file, content_type=value.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format(
self.request.event.slug.upper(), self.order.code, self.order_position.positionid,
self.output.identifier, value.extension
)
return resp
if value.type == 'text/uri-list':
resp = HttpResponseRedirect(value.file.file.read())
return resp
else:
resp = FileResponse(value.file.file, content_type=value.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format(
self.request.event.slug.upper(), self.order.code, self.order_position.positionid,
self.output.identifier, value.extension
)
return resp
elif isinstance(value, CachedCombinedTicket):
resp = FileResponse(value.file.file, content_type=value.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format(

View File

@@ -81,7 +81,7 @@
{% if line.generate_ticket %}
{% for b in download_buttons %}
<form action="{% if position_page and line.addon_to %}{% eventurl event "presale:event.order.position.download" secret=line.addon_to.web_secret order=order.code output=b.identifier pid=line.pk position=line.addon_to.positionid %}{% elif position_page %}{% eventurl event "presale:event.order.position.download" secret=line.web_secret order=order.code output=b.identifier pid=line.pk position=line.positionid %}{% else %}{% eventurl event "presale:event.order.download" secret=order.secret order=order.code output=b.identifier position=line.pk %}{% endif %}"
method="post" data-asynctask data-asynctask-download class="download-btn-form">
method="post" data-asynctask data-asynctask-download class="download-btn-form{% if b.javascript_required %} requirejs{% endif %}">
{% csrf_token %}
<button type="submit"
class="btn btn-sm {% if b.identifier == "pdf" %}btn-primary{% else %}btn-default{% endif %}">

View File

@@ -8,7 +8,9 @@ from django.contrib import messages
from django.core.files import File
from django.db import transaction
from django.db.models import Exists, OuterRef, Q, Sum
from django.http import FileResponse, Http404, JsonResponse
from django.http import (
FileResponse, Http404, HttpResponseRedirect, JsonResponse,
)
from django.shortcuts import get_object_or_404, redirect
from django.utils.decorators import method_decorator
from django.utils.functional import cached_property
@@ -155,7 +157,8 @@ class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TicketPageMixin,
'text': provider.download_button_text or 'Download',
'icon': provider.download_button_icon or 'fa-download',
'identifier': provider.identifier,
'multi': provider.multi_download_enabled
'multi': provider.multi_download_enabled,
'javascript_required': provider.javascript_required
})
return buttons
@@ -249,7 +252,8 @@ class OrderPositionDetails(EventViewMixin, OrderPositionDetailMixin, CartMixin,
'text': provider.download_button_text or 'Download',
'icon': provider.download_button_icon or 'fa-download',
'identifier': provider.identifier,
'multi': provider.multi_download_enabled
'multi': provider.multi_download_enabled,
'javascript_required': provider.javascript_required
})
return buttons
@@ -793,12 +797,16 @@ class OrderDownloadMixin:
'message': str(self.get_success_message(value))
})
if isinstance(value, CachedTicket):
resp = FileResponse(value.file.file, content_type=value.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format(
self.request.event.slug.upper(), self.order.code, self.order_position.positionid,
self.output.identifier, value.extension
)
return resp
if value.type == 'text/uri-list':
resp = HttpResponseRedirect(value.file.file.read())
return resp
else:
resp = FileResponse(value.file.file, content_type=value.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format(
self.request.event.slug.upper(), self.order.code, self.order_position.positionid,
self.output.identifier, value.extension
)
return resp
elif isinstance(value, CachedCombinedTicket):
resp = FileResponse(value.file.file, content_type=value.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format(

View File

@@ -667,6 +667,10 @@ h1 .label {
}
}
.nojs .requirejs {
display: none !important;
}
@import "../../pretixbase/scss/_rtl.scss";
@import "../../bootstrap/scss/_rtl.scss";
@import "_rtl.scss";
@import "_rtl.scss";

View File

@@ -288,6 +288,10 @@ h2 .label {
display: inline-block;
}
.nojs .requirejs {
display: none !important;
}
@import "_iframe.scss";
@import "_a11y.scss";
@import "_print.scss";