diff --git a/doc/development/api/ticketoutput.rst b/doc/development/api/ticketoutput.rst index 0af834481..73927fc17 100644 --- a/doc/development/api/ticketoutput.rst +++ b/doc/development/api/ticketoutput.rst @@ -64,4 +64,6 @@ The output class .. automethod:: generate + .. automethod:: generate_order + .. autoattribute:: download_button_text diff --git a/src/pretix/base/migrations/0057_auto_20170107_1531.py b/src/pretix/base/migrations/0057_auto_20170107_1531.py index 49d709445..64d3a06f6 100644 --- a/src/pretix/base/migrations/0057_auto_20170107_1531.py +++ b/src/pretix/base/migrations/0057_auto_20170107_1531.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import django.core.validators from django.db import migrations, models + import pretix.base.validators diff --git a/src/pretix/base/migrations/0059_cachedcombinedticket.py b/src/pretix/base/migrations/0059_cachedcombinedticket.py new file mode 100644 index 000000000..88b957056 --- /dev/null +++ b/src/pretix/base/migrations/0059_cachedcombinedticket.py @@ -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')), + ], + ), + ] diff --git a/src/pretix/base/migrations/0060_auto_20170113_1438.py b/src/pretix/base/migrations/0060_auto_20170113_1438.py new file mode 100644 index 000000000..505659e34 --- /dev/null +++ b/src/pretix/base/migrations/0060_auto_20170113_1438.py @@ -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, + ), + ] diff --git a/src/pretix/base/models/__init__.py b/src/pretix/base/models/__init__.py index 2ea64e59f..41055df9f 100644 --- a/src/pretix/base/models/__init__.py +++ b/src/pretix/base/models/__init__.py @@ -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 diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index 7973ef92f..1e10059f0 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -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) diff --git a/src/pretix/base/services/tickets.py b/src/pretix/base/services/tickets.py index 5fd82797d..b53b9b3f9 100644 --- a/src/pretix/base/services/tickets.py +++ b/src/pretix/base/services/tickets.py @@ -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 diff --git a/src/pretix/base/ticketoutput.py b/src/pretix/base/ticketoutput.py index ce8ab5f9d..fa605bf1b 100644 --- a/src/pretix/base/ticketoutput.py +++ b/src/pretix/base/ticketoutput.py @@ -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: """ diff --git a/src/pretix/presale/templates/pretixpresale/event/order.html b/src/pretix/presale/templates/pretixpresale/event/order.html index dfa6df7f0..2a5e2866e 100644 --- a/src/pretix/presale/templates/pretixpresale/event/order.html +++ b/src/pretix/presale/templates/pretixpresale/event/order.html @@ -73,6 +73,17 @@ You can download your tickets using the buttons below. Please have your ticket ready when entering the event. {% endblocktrans %} + {% if cart.positions|length > 1 %} +
+ {% trans "Download all tickets at once:" %} + {% for b in download_buttons %} + + {{ b.text }} + + {% endfor %} +
+ {% endif %} {% elif not download_buttons %}