mirror of
https://github.com/pretix/pretix.git
synced 2026-05-07 15:34:02 +00:00
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:
committed by
Raphael Michel
parent
27538d220e
commit
03c760c2bb
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 %}">
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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";
|
||||
@@ -288,6 +288,10 @@ h2 .label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.nojs .requirejs {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@import "_iframe.scss";
|
||||
@import "_a11y.scss";
|
||||
@import "_print.scss";
|
||||
|
||||
Reference in New Issue
Block a user