Fix #356 -- Download all tickets from an order

This commit is contained in:
Raphael Michel
2017-01-13 15:51:47 +01:00
parent 2ec534e32d
commit ea807239b1
11 changed files with 197 additions and 10 deletions

View File

@@ -4,6 +4,7 @@ from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
import pretix.base.validators

View File

@@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-01-13 14:07
from __future__ import unicode_literals
import django.db.models.deletion
from django.db import migrations, models
import pretix.base.models.orders
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0058_auto_20170107_1533'),
]
operations = [
migrations.CreateModel(
name='CachedCombinedTicket',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('provider', models.CharField(max_length=255)),
('type', models.CharField(max_length=255)),
('extension', models.CharField(max_length=255)),
('file', models.FileField(blank=True, null=True, upload_to=pretix.base.models.orders.cachedcombinedticket_name)),
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Order')),
],
),
]

View File

@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-01-13 14:38
from __future__ import unicode_literals
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0059_cachedcombinedticket'),
]
operations = [
migrations.AddField(
model_name='cachedcombinedticket',
name='created',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='cachedticket',
name='created',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
]

View File

@@ -12,8 +12,10 @@ from .items import (
)
from .log import LogEntry
from .orders import (
AbstractPosition, CachedTicket, CartPosition, InvoiceAddress, Order,
OrderPosition, QuestionAnswer, generate_position_secret, generate_secret,
AbstractPosition, CachedCombinedTicket, CachedTicket, CartPosition,
InvoiceAddress, Order, OrderPosition, QuestionAnswer,
cachedcombinedticket_name, cachedticket_name, generate_position_secret,
generate_secret,
)
from .organizer import Organizer, OrganizerPermission, OrganizerSetting
from .vouchers import Voucher

View File

@@ -575,12 +575,33 @@ def cachedticket_name(instance, filename: str) -> str:
)
def cachedcombinedticket_name(instance, filename: str) -> str:
secret = get_random_string(length=16, allowed_chars=string.ascii_letters + string.digits)
return 'tickets/{org}/{ev}/{code}-{prov}-{secret}.pdf'.format(
org=instance.order.event.organizer.slug,
ev=instance.order.event.slug,
prov=instance.provider,
code=instance.order.code,
secret=secret
)
class CachedTicket(models.Model):
order_position = models.ForeignKey(OrderPosition, on_delete=models.CASCADE)
provider = models.CharField(max_length=255)
type = models.CharField(max_length=255)
extension = models.CharField(max_length=255)
file = models.FileField(null=True, blank=True, upload_to=cachedticket_name)
created = models.DateTimeField(auto_now_add=True)
class CachedCombinedTicket(models.Model):
order = models.ForeignKey(Order, on_delete=models.CASCADE)
provider = models.CharField(max_length=255)
type = models.CharField(max_length=255)
extension = models.CharField(max_length=255)
file = models.FileField(null=True, blank=True, upload_to=cachedcombinedticket_name)
created = models.DateTimeField(auto_now_add=True)
@receiver(post_delete, sender=CachedTicket)

View File

@@ -5,7 +5,9 @@ from django.utils.timezone import now
from django.utils.translation import ugettext as _
from pretix.base.i18n import language
from pretix.base.models import CachedTicket, Event, Order, OrderPosition
from pretix.base.models import (
CachedCombinedTicket, CachedTicket, Event, Order, OrderPosition,
)
from pretix.base.services.async import ProfiledTask
from pretix.base.signals import register_ticket_outputs
from pretix.celery_app import app
@@ -37,6 +39,31 @@ def generate(order_position: str, provider: str):
ct.file.save(filename, ContentFile(data))
@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)
path, ext = os.path.splitext(filename)
ct.extension = ext
ct.save()
ct.file.save(filename, ContentFile(data))
class DummyRollbackException(Exception):
pass

View File

@@ -1,11 +1,14 @@
import os
import tempfile
from collections import OrderedDict
from typing import Tuple
from zipfile import ZipFile
from django import forms
from django.http import HttpRequest
from django.utils.translation import ugettext_lazy as _
from pretix.base.models import Event, OrderPosition
from pretix.base.models import Event, Order, OrderPosition
from pretix.base.settings import SettingsSandbox
@@ -29,7 +32,7 @@ class BaseTicketOutput:
"""
return self.settings.get('_enabled', as_type=bool)
def generate(self, order: OrderPosition) -> Tuple[str, str, str]:
def generate(self, position: OrderPosition) -> Tuple[str, str, str]:
"""
This method should generate the download file and return a tuple consisting of a
filename, a file type and file content. The extension will be taken from the filename
@@ -37,6 +40,29 @@ class BaseTicketOutput:
"""
raise NotImplementedError()
def generate_order(self, order: Order) -> Tuple[str, str, str]:
"""
This method is the same as order() but should not generate one file per order position
but instead one file for the full order.
This method is optional to implement. If you don't implement it, the default
implementation will offer a zip file of the generate() results for the order positions.
This method should generate a download file and return a tuple consisting of a
filename, a file type and file content. The extension will be taken from the filename
which is otherwise ignored.
"""
with tempfile.TemporaryDirectory() as d:
with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
for pos in order.positions.all():
fname, __, content = self.generate(pos)
zipf.writestr('{}-{}{}'.format(
order.code, pos.positionid, os.path.splitext(fname)[1]
), content)
with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
return '{}-{}.zip'.format(order.code, self.identifier), 'application/zip', zipf.read()
@property
def verbose_name(self) -> str:
"""

View File

@@ -73,6 +73,17 @@
You can download your tickets using the buttons below. Please have your ticket ready when entering the event.
{% endblocktrans %}
</div>
{% if cart.positions|length > 1 %}
<p>
{% trans "Download all tickets at once:" %}
{% for b in download_buttons %}
<a href="{% eventurl event "presale:event.order.download.combined" secret=order.secret order=order.code output=b.identifier %}"
class="btn btn-default btn-sm" data-asyncdownload>
<span class="fa fa-download"></span> {{ b.text }}
</a>
{% endfor %}
</p>
{% endif %}
{% elif not download_buttons %}
<div class="alert alert-info">
{% blocktrans trimmed with date=event.settings.ticket_download_date|date:"SHORT_DATE_FORMAT" %}

View File

@@ -45,6 +45,9 @@ event_patterns = [
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/pay/change',
pretix.presale.views.order.OrderPayChangeMethod.as_view(),
name='event.order.pay.change'),
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/download/(?P<output>[^/]+)$',
pretix.presale.views.order.OrderDownload.as_view(),
name='event.order.download.combined'),
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/download/(?P<position>[0-9]+)/(?P<output>[^/]+)$',
pretix.presale.views.order.OrderDownload.as_view(),
name='event.order.download'),

View File

@@ -1,3 +1,5 @@
from datetime import timedelta
from django.contrib import messages
from django.db import transaction
from django.db.models import Sum
@@ -9,12 +11,12 @@ from django.utils.translation import ugettext_lazy as _
from django.views.generic import TemplateView, View
from pretix.base.models import CachedTicket, Invoice, Order, OrderPosition
from pretix.base.models.orders import InvoiceAddress
from pretix.base.models.orders import CachedCombinedTicket, InvoiceAddress
from pretix.base.services.invoices import (
generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified,
)
from pretix.base.services.orders import cancel_order
from pretix.base.services.tickets import generate
from pretix.base.services.tickets import generate, generate_order
from pretix.base.signals import (
register_payment_providers, register_ticket_outputs,
)
@@ -508,7 +510,7 @@ class OrderDownload(EventViewMixin, OrderDetailMixin, View):
if not self.output or not self.output.is_enabled:
messages.error(request, _('You requested an invalid ticket output type.'))
return redirect(self.get_order_url())
if not self.order or not self.order_position:
if not self.order or ('position' in kwargs and not self.order_position):
raise Http404(_('Unknown order code or not authorized to access this order.'))
if self.order.status != Order.STATUS_PAID:
messages.error(request, _('Order is not paid.'))
@@ -519,6 +521,39 @@ class OrderDownload(EventViewMixin, OrderDetailMixin, View):
messages.error(request, _('Ticket download is not (yet) enabled.'))
return redirect(self.get_order_url())
if 'position' in kwargs:
return self._download_position()
else:
return self._download_order()
def _download_order(self):
try:
ct = CachedCombinedTicket.objects.filter(
order=self.order, provider=self.output.identifier
).last()
except CachedCombinedTicket.DoesNotExist:
ct = None
if not ct:
ct = CachedCombinedTicket.objects.create(
order=self.order, provider=self.output.identifier,
extension='', type='', file=None)
generate_order.apply_async(args=(self.order.id, self.output.identifier))
if 'ajax' in self.request.GET:
return HttpResponse('1' if ct and ct.file else '0')
elif not ct.file:
if now() - ct.created > timedelta(minutes=110):
generate_order.apply_async(args=(self.order.id, self.output.identifier))
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):
try:
ct = CachedTicket.objects.filter(
order_position=self.order_position, provider=self.output.identifier
@@ -532,10 +567,12 @@ class OrderDownload(EventViewMixin, OrderDetailMixin, View):
extension='', type='', file=None)
generate.apply_async(args=(self.order_position.id, self.output.identifier))
if 'ajax' in request.GET:
if 'ajax' in self.request.GET:
return HttpResponse('1' if ct and ct.file else '0')
elif not ct.file:
return render(request, "pretixbase/cachedfiles/pending.html", {})
if now() - ct.created > timedelta(minutes=110):
generate.apply_async(args=(self.order_position.id, self.output.identifier))
return render(self.request, "pretixbase/cachedfiles/pending.html", {})
else:
resp = FileResponse(ct.file.file, content_type=ct.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format(