Use regular asynctasks for order PDF generation

This commit is contained in:
Raphael Michel
2018-11-26 13:21:25 +01:00
parent cc92210dc2
commit ca59237ebf
11 changed files with 148 additions and 228 deletions

View File

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

View File

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

View File

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

View File

@@ -46,7 +46,6 @@
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/quicksetup.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/details.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/asynctask.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/asyncdownload.js" %}"></script>
<script type="text/javascript" src="{% static "colorpicker/bootstrap-colorpicker.js" %}"></script>
<script type="text/javascript" src="{% static "fileupload/jquery.ui.widget.js" %}"></script>
<script type="text/javascript" src="{% static "fileupload/jquery.fileupload.js" %}"></script>

View File

@@ -27,7 +27,6 @@
<script type="text/javascript" src="{% static "pretixcontrol/js/jquery.qrcode.min.js" %}"></script>
<script type="text/javascript" src="{% static "pretixpresale/js/ui/main.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/asynctask.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/asyncdownload.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/details.js" %}"></script>
<script type="text/javascript" src="{% static "pretixpresale/js/ui/cart.js" %}"></script>
<script type="text/javascript" src="{% static "lightbox/js/lightbox.min.js" %}"></script>

View File

@@ -60,11 +60,14 @@
<div class="download-desktop">
{% if not line.addon_to or event.settings.ticket_download_addons %}
{% for b in download_buttons %}
<a href="{% eventurl event "presale:event.order.download" secret=order.secret order=order.code output=b.identifier position=line.id %}"
class="btn btn-sm {% if b.identifier == "pdf" %}btn-primary{% else %}btn-default{% endif %}"
data-asyncdownload>
<span class="fa {{ b.icon }}"></span> {{ b.text }}
</a>
<form action="{% eventurl event "presale:event.order.download" secret=order.secret order=order.code output=b.identifier position=line.id %}"
method="post" data-asynctask data-asynctask-download class="download-btn-form">
{% csrf_token %}
<button type="submit"
class="btn btn-sm {% if b.identifier == "pdf" %}btn-primary{% else %}btn-default{% endif %}">
<span class="fa {{ b.icon }}"></span> {{ b.text }}
</button>
</form>
{% endfor %}
{% endif %}
</div>
@@ -150,11 +153,14 @@
<div class="download-mobile">
{% if not line.addon_to or event.settings.ticket_download_addons %}
{% for b in download_buttons %}
<a href="{% eventurl event "presale:event.order.download" secret=order.secret order=order.code output=b.identifier position=line.id %}"
class="btn btn-sm {% if b.identifier == "pdf" %}btn-primary{% else %}btn-default{% endif %}"
data-asyncdownload>
<span class="fa {{ b.icon }}"></span> {{ b.text }}
</a>
<form action="{% eventurl event "presale:event.order.download" secret=order.secret order=order.code output=b.identifier position=line.id %}"
method="post" data-asynctask data-asynctask-download class="download-btn-form">
{% csrf_token %}
<button type="submit"
class="btn btn-sm {% if b.identifier == "pdf" %}btn-primary{% else %}btn-default{% endif %}">
<span class="fa {{ b.icon }}"></span> {{ b.text }}
</button>
</form>
{% endfor %}
{% endif %}
</div>

View File

@@ -95,11 +95,14 @@
{% trans "Download all tickets at once:" %}
{% for b in download_buttons %}
{% if b.multi %}
<a href="{% eventurl event "presale:event.order.download.combined" secret=order.secret order=order.code output=b.identifier %}"
class="btn btn-sm {% if b.identifier == "pdf" %}btn-primary{% else %}btn-default{% endif %}"
data-asyncdownload>
<span class="fa {{ b.icon }}"></span> {{ b.text }}
</a>
<form action="{% eventurl event "presale:event.order.download.combined" secret=order.secret order=order.code output=b.identifier %}"
method="post" data-asynctask data-asynctask-download class="download-btn-form">
{% csrf_token %}
<button type="submit"
class="btn btn-sm {% if b.identifier == "pdf" %}btn-primary{% else %}btn-default{% endif %}">
<span class="fa {{ b.icon }}"></span> {{ b.text }}
</button>
</form>
{% endif %}
{% endfor %}
</p>

View File

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

View File

@@ -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();
});
});

View File

@@ -278,5 +278,9 @@ details summary {
transform: rotate(180deg);
}
form.download-btn-form {
display: inline;
}
@import "_iframe.scss";
@import "_a11y.scss";

View File

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