diff --git a/src/pretix/api/views/order.py b/src/pretix/api/views/order.py
index 2687bb97e..39c92b90c 100644
--- a/src/pretix/api/views/order.py
+++ b/src/pretix/api/views/order.py
@@ -25,8 +25,8 @@ from pretix.api.serializers.order import (
OrderRefundSerializer, OrderSerializer,
)
from pretix.base.models import (
- Device, Invoice, Order, OrderPayment, OrderPosition, OrderRefund, Quota,
- TeamAPIToken,
+ CachedCombinedTicket, CachedTicket, Device, Invoice, Order, OrderPayment,
+ OrderPosition, OrderRefund, Quota, TeamAPIToken,
)
from pretix.base.payment import PaymentException
from pretix.base.services.invoices import (
@@ -38,9 +38,7 @@ from pretix.base.services.orders import (
OrderChangeManager, OrderError, approve_order, cancel_order, deny_order,
extend_order, mark_order_expired, mark_order_refunded,
)
-from pretix.base.services.tickets import (
- get_cachedticket_for_order, get_cachedticket_for_position,
-)
+from pretix.base.services.tickets import generate
from pretix.base.signals import order_placed, register_ticket_outputs
@@ -130,9 +128,11 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
if order.status != Order.STATUS_PAID:
raise PermissionDenied("Downloads are not available for unpaid orders.")
- ct = get_cachedticket_for_order(order, provider.identifier)
-
- if not ct.file:
+ ct = CachedCombinedTicket.objects.filter(
+ order=order, provider=provider.identifier, file__isnull=False
+ ).last()
+ if not ct or not ct.file:
+ generate.apply_async(args=('order', order.pk, provider.identifier))
raise RetryException()
else:
resp = FileResponse(ct.file.file, content_type=ct.type)
@@ -447,9 +447,11 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
if not pos.item.admission and not request.event.settings.ticket_download_nonadm:
raise PermissionDenied("Downloads are not enabled for non-admission products.")
- ct = get_cachedticket_for_position(pos, provider.identifier)
-
- if not ct.file:
+ ct = CachedTicket.objects.filter(
+ order_position=pos, provider=provider.identifier, file__isnull=False
+ ).last()
+ if not ct or not ct.file:
+ generate.apply_async(args=('orderposition', pos.pk, provider.identifier))
raise RetryException()
else:
resp = FileResponse(ct.file.file, content_type=ct.type)
diff --git a/src/pretix/base/services/cleanup.py b/src/pretix/base/services/cleanup.py
index 06eb4ab14..3a3ba4b0d 100644
--- a/src/pretix/base/services/cleanup.py
+++ b/src/pretix/base/services/cleanup.py
@@ -31,3 +31,7 @@ def clean_cached_tickets(sender, **kwargs):
cf.delete()
for cf in CachedCombinedTicket.objects.filter(created__lte=now() - timedelta(days=30)):
cf.delete()
+ for cf in CachedTicket.objects.filter(created__lte=now() - timedelta(minutes=30), file__isnull=True):
+ cf.delete()
+ for cf in CachedCombinedTicket.objects.filter(created__lte=now() - timedelta(minutes=30), file__isnull=True):
+ cf.delete()
diff --git a/src/pretix/base/services/tickets.py b/src/pretix/base/services/tickets.py
index b28550bd7..23e7d2b02 100644
--- a/src/pretix/base/services/tickets.py
+++ b/src/pretix/base/services/tickets.py
@@ -1,6 +1,5 @@
import logging
import os
-from datetime import timedelta
from django.core.files.base import ContentFile
from django.utils.timezone import now
@@ -20,54 +19,48 @@ from pretix.helpers.database import rolledback_transaction
logger = logging.getLogger(__name__)
-@app.task(base=ProfiledTask)
-def generate(order_position: str, provider: str):
+def generate_orderposition(order_position: int, provider: str):
order_position = OrderPosition.objects.select_related('order', 'order__event').get(id=order_position)
- try:
- ct = CachedTicket.objects.get(order_position=order_position, provider=provider)
- except CachedTicket.MultipleObjectsReturned:
- CachedTicket.objects.filter(order_position=order_position, provider=provider).delete()
- ct = CachedTicket.objects.create(order_position=order_position, provider=provider, extension='',
- type='', file=None)
- except CachedTicket.DoesNotExist:
- ct = CachedTicket.objects.create(order_position=order_position, provider=provider, extension='',
- type='', file=None)
with language(order_position.order.locale):
responses = register_ticket_outputs.send(order_position.order.event)
for receiver, response in responses:
prov = response(order_position.order.event)
if prov.identifier == provider:
- filename, ct.type, data = prov.generate(order_position)
+ filename, ttype, data = prov.generate(order_position)
path, ext = os.path.splitext(filename)
- ct.extension = ext
- ct.save()
+ for ct in CachedTicket.objects.filter(order_position=order_position, provider=provider):
+ ct.delete()
+ ct = CachedTicket.objects.create(order_position=order_position, provider=provider,
+ extension=ext, type=ttype, file=None)
ct.file.save(filename, ContentFile(data))
+ return ct.pk
-@app.task(base=ProfiledTask)
def generate_order(order: int, provider: str):
order = Order.objects.select_related('event').get(id=order)
- try:
- ct = CachedCombinedTicket.objects.get(order=order, provider=provider)
- except CachedCombinedTicket.MultipleObjectsReturned:
- CachedCombinedTicket.objects.filter(order=order, provider=provider).delete()
- ct = CachedCombinedTicket.objects.create(order=order, provider=provider, extension='',
- type='', file=None)
- except CachedCombinedTicket.DoesNotExist:
- ct = CachedCombinedTicket.objects.create(order=order, provider=provider, extension='',
- type='', file=None)
with language(order.locale):
responses = register_ticket_outputs.send(order.event)
for receiver, response in responses:
prov = response(order.event)
if prov.identifier == provider:
- filename, ct.type, data = prov.generate_order(order)
+ filename, ttype, data = prov.generate_order(order)
path, ext = os.path.splitext(filename)
- ct.extension = ext
- ct.save()
+ for ct in CachedCombinedTicket.objects.filter(order=order, provider=provider):
+ ct.delete()
+ ct = CachedCombinedTicket.objects.create(order=order, provider=provider, extension=ext,
+ type=ttype, file=None)
ct.file.save(filename, ContentFile(data))
+ return ct.pk
+
+
+@app.task(base=ProfiledTask)
+def generate(model: str, pk: int, provider: str):
+ if model == 'order':
+ return generate_order(pk, provider)
+ elif model == 'orderposition':
+ return generate_orderposition(pk, provider)
class DummyRollbackException(Exception):
@@ -103,56 +96,6 @@ def preview(event: int, provider: str):
return prov.generate(p)
-def get_cachedticket_for_position(pos, identifier, generate_async=True):
- apply_method = 'apply_async' if generate_async else 'apply'
- try:
- ct = CachedTicket.objects.filter(
- order_position=pos, provider=identifier
- ).last()
- except CachedTicket.DoesNotExist:
- ct = None
-
- if not ct:
- ct = CachedTicket.objects.create(
- order_position=pos, provider=identifier,
- extension='', type='', file=None)
- getattr(generate, apply_method)(args=(pos.id, identifier))
- if not generate_async:
- ct.refresh_from_db()
-
- if not ct.file:
- if now() - ct.created > timedelta(minutes=5):
- getattr(generate, apply_method)(args=(pos.id, identifier))
- if not generate_async:
- ct.refresh_from_db()
- return ct
-
-
-def get_cachedticket_for_order(order, identifier, generate_async=True):
- apply_method = 'apply_async' if generate_async else 'apply'
- try:
- ct = CachedCombinedTicket.objects.filter(
- order=order, provider=identifier
- ).last()
- except CachedCombinedTicket.DoesNotExist:
- ct = None
-
- if not ct:
- ct = CachedCombinedTicket.objects.create(
- order=order, provider=identifier,
- extension='', type='', file=None)
- getattr(generate_order, apply_method)(args=(order.id, identifier))
- if not generate_async:
- ct.refresh_from_db()
-
- if not ct.file:
- if now() - ct.created > timedelta(minutes=5):
- getattr(generate_order, apply_method)(args=(order.id, identifier))
- if not generate_async:
- ct.refresh_from_db()
- return ct
-
-
def get_tickets_for_order(order):
can_download = all([r for rr, r in allow_ticket_download.send(order.event, order=order)])
if not can_download:
@@ -174,7 +117,12 @@ def get_tickets_for_order(order):
if p.multi_download_enabled:
try:
- ct = get_cachedticket_for_order(order, p.identifier, generate_async=False)
+ ct = CachedCombinedTicket.objects.filter(
+ order=order, provider=p.identifier, file__isnull=False
+ ).last()
+ if not ct or not ct.file:
+ retval = generate.apply(args=('order', order.pk, p.identifier))
+ ct = CachedCombinedTicket.objects.get(pk=retval.value)
tickets.append((
"{}-{}-{}{}".format(
order.event.slug.upper(), order.code, ct.provider, ct.extension,
@@ -190,7 +138,12 @@ def get_tickets_for_order(order):
if not pos.item.admission and not order.event.settings.ticket_download_nonadm:
continue
try:
- ct = get_cachedticket_for_position(pos, p.identifier, generate_async=False)
+ ct = CachedTicket.objects.filter(
+ order_position=pos, provider=p.identifier, file__isnull=False
+ ).last()
+ if not ct or not ct.file:
+ retval = generate.apply(args=('orderposition', pos.pk, p.identifier))
+ ct = CachedTicket.objects.get(pk=retval.value)
tickets.append((
"{}-{}-{}-{}{}".format(
order.event.slug.upper(), order.code, pos.positionid, ct.provider, ct.extension,
diff --git a/src/pretix/control/templates/pretixcontrol/base.html b/src/pretix/control/templates/pretixcontrol/base.html
index 6dcf683dd..92b16b702 100644
--- a/src/pretix/control/templates/pretixcontrol/base.html
+++ b/src/pretix/control/templates/pretixcontrol/base.html
@@ -46,7 +46,6 @@
-
diff --git a/src/pretix/presale/templates/pretixpresale/base.html b/src/pretix/presale/templates/pretixpresale/base.html
index 82cae0b90..c046a04e7 100644
--- a/src/pretix/presale/templates/pretixpresale/base.html
+++ b/src/pretix/presale/templates/pretixpresale/base.html
@@ -27,7 +27,6 @@
-
diff --git a/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html b/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html
index fe5272ea0..00b2737c2 100644
--- a/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html
+++ b/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html
@@ -60,11 +60,14 @@
{% if not line.addon_to or event.settings.ticket_download_addons %}
{% for b in download_buttons %}
-
- {{ b.text }}
-
+
{% endfor %}
{% endif %}
@@ -150,11 +153,14 @@
{% if not line.addon_to or event.settings.ticket_download_addons %}
{% for b in download_buttons %}
-
- {{ b.text }}
-
+
{% endfor %}
{% endif %}
diff --git a/src/pretix/presale/templates/pretixpresale/event/order.html b/src/pretix/presale/templates/pretixpresale/event/order.html
index 3af584f38..aef97a542 100644
--- a/src/pretix/presale/templates/pretixpresale/event/order.html
+++ b/src/pretix/presale/templates/pretixpresale/event/order.html
@@ -95,11 +95,14 @@
{% trans "Download all tickets at once:" %}
{% for b in download_buttons %}
{% if b.multi %}
-
- {{ b.text }}
-
+
{% endif %}
{% endfor %}
diff --git a/src/pretix/presale/views/order.py b/src/pretix/presale/views/order.py
index b1c266e9b..41a7fc35a 100644
--- a/src/pretix/presale/views/order.py
+++ b/src/pretix/presale/views/order.py
@@ -2,12 +2,13 @@ import mimetypes
import os
from decimal import Decimal
+from django.conf import settings
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.shortcuts import get_object_or_404, redirect, render
+from django.shortcuts import get_object_or_404, redirect
from django.utils.decorators import method_decorator
from django.utils.functional import cached_property
from django.utils.timezone import now
@@ -25,9 +26,7 @@ from pretix.base.services.invoices import (
invoice_qualified,
)
from pretix.base.services.orders import cancel_order
-from pretix.base.services.tickets import (
- get_cachedticket_for_order, get_cachedticket_for_position,
-)
+from pretix.base.services.tickets import generate
from pretix.base.signals import allow_ticket_download, register_ticket_outputs
from pretix.base.views.mixins import OrderQuestionsViewMixin
from pretix.base.views.tasks import AsyncAction
@@ -627,7 +626,14 @@ class AnswerDownload(EventViewMixin, OrderDetailMixin, View):
@method_decorator(xframe_options_exempt, 'dispatch')
-class OrderDownload(EventViewMixin, OrderDetailMixin, View):
+class OrderDownload(EventViewMixin, OrderDetailMixin, AsyncAction, View):
+ task = generate
+
+ def get_success_url(self, value):
+ return self.get_self_url()
+
+ def get_error_url(self):
+ return self.get_order_url()
def get_self_url(self):
return eventreverse(self.request.event,
@@ -652,18 +658,15 @@ class OrderDownload(EventViewMixin, OrderDetailMixin, View):
except OrderPosition.DoesNotExist:
return None
- def error(self, msg):
- messages.error(self.request, msg)
- if "ajax" in self.request.POST or "ajax" in self.request.GET:
- return JsonResponse({
- 'ready': True,
- 'success': False,
- 'redirect': self.get_order_url(),
- 'message': msg,
- })
- return redirect(self.get_order_url())
-
def get(self, request, *args, **kwargs):
+ if 'async_id' in request.GET and settings.HAS_CELERY:
+ return self.get_result(request)
+ ct = self.get_last_ct()
+ if ct:
+ return self.success(ct)
+ return self.http_method_not_allowed(request)
+
+ def post(self, request, *args, **kwargs):
if not self.output or not self.output.is_enabled:
return self.error(_('You requested an invalid ticket output type.'))
if not self.order or ('position' in kwargs and not self.order_position):
@@ -675,47 +678,52 @@ class OrderDownload(EventViewMixin, OrderDetailMixin, View):
if 'position' in kwargs and (not self.order_position.item.admission and not self.request.event.settings.ticket_download_nonadm):
return self.error(_('Ticket download is not enabled for non-admission products.'))
- if 'position' in kwargs:
- return self._download_position()
- else:
- return self._download_order()
+ ct = self.get_last_ct()
+ if ct:
+ return self.success(ct)
+ return self.do('orderposition' if 'position' in kwargs else 'order',
+ self.order_position.pk if 'position' in kwargs else self.order.pk,
+ self.output.identifier)
- def _download_order(self):
- ct = get_cachedticket_for_order(self.order, self.output.identifier)
+ def get_success_message(self, value):
+ return ""
- if 'ajax' in self.request.GET:
+ def success(self, value):
+ if "ajax" in self.request.POST or "ajax" in self.request.GET:
return JsonResponse({
- 'ready': bool(ct and ct.file),
- 'success': False,
- 'redirect': self.get_self_url()
+ 'ready': True,
+ 'success': True,
+ 'redirect': self.get_success_url(value),
+ 'message': str(self.get_success_message(value))
})
- elif not ct.file:
- return render(self.request, "pretixbase/cachedfiles/pending.html", {})
- else:
- resp = FileResponse(ct.file.file, content_type=ct.type)
- resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format(
- self.request.event.slug.upper(), self.order.code, self.output.identifier, ct.extension
- )
- return resp
-
- def _download_position(self):
- ct = get_cachedticket_for_position(self.order_position, self.output.identifier)
-
- if 'ajax' in self.request.GET:
- return JsonResponse({
- 'ready': bool(ct and ct.file),
- 'success': False,
- 'redirect': self.get_self_url()
- })
- elif not ct.file:
- return render(self.request, "pretixbase/cachedfiles/pending.html", {})
- else:
- resp = FileResponse(ct.file.file, content_type=ct.type)
+ 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, ct.extension
+ 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(
+ self.request.event.slug.upper(), self.order.code, self.output.identifier, value.extension
+ )
+ return resp
+ else:
+ return redirect(self.get_self_url())
+
+ def get_last_ct(self):
+ if 'position' in self.kwargs:
+ ct = CachedTicket.objects.filter(
+ order_position=self.order_position, provider=self.output.identifier, file__isnull=False
+ ).last()
+ else:
+ ct = CachedCombinedTicket.objects.filter(
+ order=self.order, provider=self.output.identifier, file__isnull=False
+ ).last()
+ if not ct or not ct.file:
+ return None
+ return ct
@method_decorator(xframe_options_exempt, 'dispatch')
diff --git a/src/pretix/static/pretixbase/js/asyncdownload.js b/src/pretix/static/pretixbase/js/asyncdownload.js
deleted file mode 100644
index 95b86c953..000000000
--- a/src/pretix/static/pretixbase/js/asyncdownload.js
+++ /dev/null
@@ -1,61 +0,0 @@
-/*global $, waitingDialog, gettext */
-var async_dl_url = null;
-var async_dl_timeout = null;
-
-function async_dl_check() {
- "use strict";
- $.ajax(
- {
- 'type': 'GET',
- 'url': async_dl_url + '?ajax=1',
- 'success': async_dl_check_callback,
- 'error': async_dl_check_error,
- 'context': this,
- 'dataType': 'json'
- }
- );
-}
-
-function async_dl_check_callback(data, jqXHR, status) {
- "use strict";
- if (data.ready && data.redirect) {
- $("body").data('ajaxing', false);
- location.href = data.redirect;
- waitingDialog.hide();
- return;
- }
- async_dl_timeout = window.setTimeout(async_dl_check, 250);
- $("#loadingmodal p").text(gettext('Your request has been queued on the server and will now be ' +
- 'processed. If this takes longer than two minutes, please contact us or go ' +
- 'back in your browser and try again.'));
-}
-
-function async_dl_check_error(jqXHR, textStatus, errorThrown) {
- "use strict";
- $("body").data('ajaxing', false);
- waitingDialog.hide();
- var c = $(jqXHR.responseText).filter('.container');
- if (c.length > 0) {
- ajaxErrDialog.show(c.first().html());
- } else if (jqXHR.status >= 400) {
- alert(gettext('An error of type {code} occurred.').replace(/\{code\}/, jqXHR.status));
- }
-}
-
-$(function () {
- "use strict";
- $("body").on('click', 'a[data-asyncdownload]', function (e) {
- e.preventDefault();
- if ($("body").data('ajaxing')) {
- return;
- }
- async_dl_url = $(this).attr("href");
- $("body").data('ajaxing', true);
- waitingDialog.show(gettext('We are processing your request …'));
- $("#loadingmodal p").text(gettext('We are currently sending your request to the server. If this takes longer ' +
- 'than one minute, please check your internet connection and then reload ' +
- 'this page and try again.'));
-
- async_dl_check();
- });
-});
diff --git a/src/pretix/static/pretixpresale/scss/main.scss b/src/pretix/static/pretixpresale/scss/main.scss
index 8307c38a7..58933cb56 100644
--- a/src/pretix/static/pretixpresale/scss/main.scss
+++ b/src/pretix/static/pretixpresale/scss/main.scss
@@ -278,5 +278,9 @@ details summary {
transform: rotate(180deg);
}
+form.download-btn-form {
+ display: inline;
+}
+
@import "_iframe.scss";
@import "_a11y.scss";
diff --git a/src/tests/presale/test_orders.py b/src/tests/presale/test_orders.py
index 807058614..c5598db84 100644
--- a/src/tests/presale/test_orders.py
+++ b/src/tests/presale/test_orders.py
@@ -313,9 +313,10 @@ class OrdersTest(TestCase):
self.order.status = Order.STATUS_PENDING
self.order.save()
self.event.settings.set('ticket_download_pending', True)
- response = self.client.get(
+ response = self.client.post(
'/%s/%s/order/%s/%s/download/%d/testdummy' % (self.orga.slug, self.event.slug, self.order.code,
self.order.secret, self.ticket_pos.pk),
+ follow=True
)
assert response.status_code == 200
@@ -327,7 +328,7 @@ class OrdersTest(TestCase):
self.order.require_approval = True
self.order.save()
self.event.settings.set('ticket_download_pending', True)
- response = self.client.get(
+ response = self.client.post(
'/%s/%s/order/%s/%s/download/%d/testdummy' % (self.orga.slug, self.event.slug, self.order.code,
self.order.secret, self.ticket_pos.pk),
)
@@ -339,7 +340,7 @@ class OrdersTest(TestCase):
def test_orders_download(self):
self.event.settings.set('ticket_download', True)
del self.event.settings['ticket_download_date']
- response = self.client.get(
+ response = self.client.post(
'/%s/%s/order/%s/%s/download/%d/pdf' % (self.orga.slug, self.event.slug, self.order.code,
self.order.secret, self.ticket_pos.pk),
follow=True)
@@ -348,13 +349,13 @@ class OrdersTest(TestCase):
self.order.secret),
target_status_code=200)
- response = self.client.get(
+ response = self.client.post(
'/%s/%s/order/ABC/123/download/%d/testdummy' % (self.orga.slug, self.event.slug,
self.ticket_pos.pk)
)
assert response.status_code == 404
- response = self.client.get(
+ response = self.client.post(
'/%s/%s/order/%s/%s/download/%d/testdummy' % (self.orga.slug, self.event.slug, self.order.code,
self.order.secret, self.ticket_pos.pk),
follow=True
@@ -366,14 +367,15 @@ class OrdersTest(TestCase):
self.order.status = Order.STATUS_PAID
self.order.save()
- response = self.client.get(
+ response = self.client.post(
'/%s/%s/order/%s/%s/download/%d/testdummy' % (self.orga.slug, self.event.slug, self.order.code,
self.order.secret, self.ticket_pos.pk),
+ follow=True
)
assert response.status_code == 200
self.event.settings.set('ticket_download_date', now() + datetime.timedelta(days=1))
- response = self.client.get(
+ response = self.client.post(
'/%s/%s/order/%s/%s/download/%d/testdummy' % (self.orga.slug, self.event.slug, self.order.code,
self.order.secret, self.ticket_pos.pk),
follow=True
@@ -388,7 +390,7 @@ class OrdersTest(TestCase):
self.event.settings.set('ticket_download_date', RelativeDateWrapper(RelativeDate(
base_date_name='date_from', days_before=2, time=None
)))
- response = self.client.get(
+ response = self.client.post(
'/%s/%s/order/%s/%s/download/%d/testdummy' % (self.orga.slug, self.event.slug, self.order.code,
self.order.secret, self.ticket_pos.pk),
follow=True
@@ -399,14 +401,15 @@ class OrdersTest(TestCase):
target_status_code=200)
del self.event.settings['ticket_download_date']
- response = self.client.get(
+ response = self.client.post(
'/%s/%s/order/%s/%s/download/%d/testdummy' % (self.orga.slug, self.event.slug, self.order.code,
self.order.secret, self.ticket_pos.pk),
+ follow=True
)
assert response.status_code == 200
self.event.settings.set('ticket_download', False)
- response = self.client.get(
+ response = self.client.post(
'/%s/%s/order/%s/%s/download/%d/testdummy' % (self.orga.slug, self.event.slug, self.order.code,
self.order.secret, self.ticket_pos.pk),
follow=True