mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
Fix #356 -- Download all tickets from an order
This commit is contained in:
@@ -4,6 +4,7 @@ from __future__ import unicode_literals
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.validators
|
||||
|
||||
|
||||
|
||||
29
src/pretix/base/migrations/0059_cachedcombinedticket.py
Normal file
29
src/pretix/base/migrations/0059_cachedcombinedticket.py
Normal 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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
28
src/pretix/base/migrations/0060_auto_20170113_1438.py
Normal file
28
src/pretix/base/migrations/0060_auto_20170113_1438.py
Normal 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,
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
|
||||
@@ -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" %}
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user