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 }} - +
+ {% csrf_token %} + +
{% endfor %} {% endif %}
@@ -150,11 +153,14 @@
{% if not line.addon_to or event.settings.ticket_download_addons %} {% for b in download_buttons %} - - {{ b.text }} - +
+ {% csrf_token %} + +
{% 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 }} - +
+ {% csrf_token %} + +
{% 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