diff --git a/src/pretix/base/__init__.py b/src/pretix/base/__init__.py index adaa408c32..89086bc87f 100644 --- a/src/pretix/base/__init__.py +++ b/src/pretix/base/__init__.py @@ -8,6 +8,7 @@ class PretixBaseConfig(AppConfig): def ready(self): from . import exporter # NOQA from . import payment # NOQA + from .services import export, mail # NOQA try: from .celery import app as celery_app # NOQA diff --git a/src/pretix/base/exporter.py b/src/pretix/base/exporter.py index 2eb07f5332..4b8c61e526 100644 --- a/src/pretix/base/exporter.py +++ b/src/pretix/base/exporter.py @@ -1,3 +1,7 @@ +import decimal +import json + +from django.core.serializers.json import DjangoJSONEncoder from django.dispatch import receiver from django.http import HttpRequest, HttpResponse, JsonResponse @@ -58,13 +62,13 @@ class BaseExporter: """ return {} - def render(self, request: HttpRequest) -> HttpResponse: + def render(self, form_data: dict) -> tuple: """ - Render the exported file and return a request that either contains the file - or redirects to it. + Render the exported file and return a tuple consisting of a filename, a file type + and file content. - :type request: HttpRequest - :param request: The HTTP request of the user requesting the export + :type form_data: dict + :param form_data: The form data of the export details form """ raise NotImplementedError() # NOQA @@ -73,7 +77,7 @@ class JSONExporter(BaseExporter): identifier = 'json' verbose_name = 'JSON' - def render(self, request): + def render(self, form_data): jo = { 'event': { 'name': str(self.event.name), @@ -151,7 +155,7 @@ class JSONExporter(BaseExporter): } } - return JsonResponse(jo) + return 'pretixdata.json', 'application/json', json.dumps(jo, cls=DjangoJSONEncoder) @receiver(register_data_exporters, dispatch_uid="exporter_json") diff --git a/src/pretix/base/migrations/0009_auto_20150915_2003.py b/src/pretix/base/migrations/0009_auto_20150915_2003.py new file mode 100644 index 0000000000..36e7142af8 --- /dev/null +++ b/src/pretix/base/migrations/0009_auto_20150915_2003.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import versions.models +from django.db import migrations, models + +import pretix.base.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0008_auto_20150804_1357'), + ] + + operations = [ + migrations.CreateModel( + name='CachedFile', + fields=[ + ('id', models.UUIDField(primary_key=True, serialize=False)), + ('expires', models.DateTimeField(null=True, blank=True)), + ('date', models.DateTimeField(null=True, blank=True)), + ('filename', models.CharField(max_length=255)), + ('file', models.FileField(null=True, upload_to=pretix.base.models.cachedfile_name, blank=True)), + ], + ), + migrations.CreateModel( + name='CachedTicket', + fields=[ + ('id', models.AutoField(primary_key=True, auto_created=True, verbose_name='ID', serialize=False)), + ('provider', models.CharField(max_length=255)), + ('cachedfile', models.ForeignKey(to='pretixbase.CachedFile')), + ('order', models.ForeignKey(to='pretixbase.Order')), + ], + ), + migrations.AlterModelOptions( + name='itemcategory', + options={'verbose_name_plural': 'Product categories', 'ordering': ('position', 'version_birth_date'), 'verbose_name': 'Product category'}, + ), + migrations.AlterModelOptions( + name='propertyvalue', + options={'verbose_name_plural': 'Property values', 'ordering': ('position', 'version_birth_date'), 'verbose_name': 'Property value'}, + ), + migrations.AlterField( + model_name='orderposition', + name='item', + field=versions.models.VersionedForeignKey(to='pretixbase.Item', verbose_name='Item', related_name='positions'), + ), + migrations.AlterField( + model_name='user', + name='locale', + field=models.CharField(choices=[('en', 'English'), ('de', 'German'), ('de-informal', 'German (informal)')], max_length=50, default='en', verbose_name='Language'), + ), + migrations.AddField( + model_name='order', + name='tickets', + field=models.ManyToManyField(to='pretixbase.CachedFile', through='pretixbase.CachedTicket'), + ), + ] diff --git a/src/pretix/base/migrations/0010_cachedfile_type.py b/src/pretix/base/migrations/0010_cachedfile_type.py new file mode 100644 index 0000000000..db12c7c4d8 --- /dev/null +++ b/src/pretix/base/migrations/0010_cachedfile_type.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0009_auto_20150915_2003'), + ] + + operations = [ + migrations.AddField( + model_name='cachedfile', + name='type', + field=models.CharField(default='text/plain', max_length=255), + preserve_default=False, + ), + ] diff --git a/src/pretix/base/migrations/0011_auto_20150915_2020.py b/src/pretix/base/migrations/0011_auto_20150915_2020.py new file mode 100644 index 0000000000..6d7df2d2e7 --- /dev/null +++ b/src/pretix/base/migrations/0011_auto_20150915_2020.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import uuid + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0010_cachedfile_type'), + ] + + operations = [ + migrations.AlterField( + model_name='cachedfile', + name='id', + field=models.UUIDField(serialize=False, primary_key=True, default=uuid.uuid4), + ), + ] diff --git a/src/pretix/base/migrations/0012_remove_order_tickets.py b/src/pretix/base/migrations/0012_remove_order_tickets.py new file mode 100644 index 0000000000..7486be3f8e --- /dev/null +++ b/src/pretix/base/migrations/0012_remove_order_tickets.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0011_auto_20150915_2020'), + ] + + operations = [ + migrations.RemoveField( + model_name='order', + name='tickets', + ), + ] diff --git a/src/pretix/base/models.py b/src/pretix/base/models.py index 20544ea0ba..67a9f8f5ad 100644 --- a/src/pretix/base/models.py +++ b/src/pretix/base/models.py @@ -291,6 +291,22 @@ class User(AbstractBaseUser, PermissionsMixin): return self.identifier # NOQA +def cachedfile_name(instance, filename): + return 'cachedfiles/%s.%s' % (instance.id, filename.split('.')[-1]) + + +class CachedFile(models.Model): + """ + A cached file (e.g. pre-generated ticket PDF) + """ + id = models.UUIDField(primary_key=True, default=uuid.uuid4) + expires = models.DateTimeField(null=True, blank=True) + date = models.DateTimeField(null=True, blank=True) + filename = models.CharField(max_length=255) + type = models.CharField(max_length=255) + file = models.FileField(null=True, blank=True, upload_to=cachedfile_name) + + class Organizer(Versionable): """ This model represents an entity organizing events, e.g. a company, institution, @@ -1687,6 +1703,12 @@ class Order(Versionable): return True, quotas_locked +class CachedTicket(models.Model): + order = models.ForeignKey(Order, on_delete=models.CASCADE) + cachedfile = models.ForeignKey(CachedFile, on_delete=models.CASCADE) + provider = models.CharField(max_length=255) + + class QuestionAnswer(Versionable): """ The answer to a Question, connected to an OrderPosition or CartPosition. diff --git a/src/pretix/base/services/export.py b/src/pretix/base/services/export.py new file mode 100644 index 0000000000..ccfeba7b54 --- /dev/null +++ b/src/pretix/base/services/export.py @@ -0,0 +1,23 @@ +from django.conf import settings +from django.core.files.base import ContentFile + +from pretix.base.models import CachedFile, Event, cachedfile_name +from pretix.base.signals import register_data_exporters + + +def export(event, fileid, provider, form_data): + event = Event.objects.current.get(identity=event) + file = CachedFile.objects.get(id=fileid) + responses = register_data_exporters.send(event) + for receiver, response in responses: + ex = response(event) + if ex.identifier == provider: + file.filename, file.type, data = ex.render(form_data) + file.file.save(cachedfile_name(file, file.filename), ContentFile(data)) + + +if settings.HAS_CELERY: + from pretix.celery import app + + export_task = app.task(export) + export = lambda *args, **kwargs: export_task.apply_async(args=args, kwargs=kwargs) diff --git a/src/pretix/base/services/mail.py b/src/pretix/base/services/mail.py index 1f4615d8c9..37ec41f867 100644 --- a/src/pretix/base/services/mail.py +++ b/src/pretix/base/services/mail.py @@ -82,7 +82,7 @@ def mail_send(to, subject, body, sender): return False -if settings.HAS_CELERY: +if settings.HAS_CELERY and settings.EMAIL_BACKEND != 'django.core.mail.outbox': from pretix.celery import app mail_send_task = app.task(mail_send) diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py index dccc84d97c..53dd42b435 100644 --- a/src/pretix/control/views/orders.py +++ b/src/pretix/control/views/orders.py @@ -1,3 +1,4 @@ +from datetime import timedelta from itertools import groupby from django import forms @@ -11,7 +12,8 @@ from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ from django.views.generic import DetailView, ListView, TemplateView, View -from pretix.base.models import Item, Order, Quota +from pretix.base.models import CachedFile, Item, Order, Quota +from pretix.base.services.export import export from pretix.base.services.orders import mark_order_paid from pretix.base.services.stats import order_overview from pretix.base.signals import ( @@ -326,4 +328,9 @@ class ExportView(EventPermissionRequiredMixin, TemplateView): messages.error(self.request, _('There was a problem processing your input. See below for error details.')) return self.get(*args, **kwargs) - return self.exporter.render(self.request) + cf = CachedFile() + cf.date = now() + cf.expires = now() + timedelta(days=3) + cf.save() + export(self.request.event.id, str(cf.id), self.exporter.identifier, self.exporter.form.cleaned_data) + return redirect(reverse('presale:cachedfile.download', kwargs={'id': cf.id})) diff --git a/src/pretix/plugins/reports/exporters.py b/src/pretix/plugins/reports/exporters.py index d6f38c9fef..82805ad7fe 100644 --- a/src/pretix/plugins/reports/exporters.py +++ b/src/pretix/plugins/reports/exporters.py @@ -204,11 +204,6 @@ class OverviewReportExporter(BaseExporter): identifier = 'pdfreport' verbose_name = _('Order overview (PDF)') - def render(self, request): - response = HttpResponse(content_type='application/pdf') - response['Content-Disposition'] = 'inline; filename="report-%s.pdf"' % request.event.slug - - report = OverviewReport(request.event) - - response.write(report.create()) - return response + def render(self, form_data): + report = OverviewReport(self.event) + return 'report-%s.pdf' % self.event.slug, 'application/pdf', report.create() diff --git a/src/pretix/presale/middleware.py b/src/pretix/presale/middleware.py index 96f73d4b61..a674e39a2f 100644 --- a/src/pretix/presale/middleware.py +++ b/src/pretix/presale/middleware.py @@ -19,4 +19,4 @@ class EventMiddleware: organizer__slug=url.kwargs['organizer'], ).select_related('organizer')[0] except IndexError: - return HttpResponseNotFound() # TODO: Provide error message + return HttpResponseNotFound('Unknown event') # TODO: Provide error message diff --git a/src/pretix/presale/templates/pretixpresale/cachedfiles/pending.html b/src/pretix/presale/templates/pretixpresale/cachedfiles/pending.html new file mode 100644 index 0000000000..3ab082ec9d --- /dev/null +++ b/src/pretix/presale/templates/pretixpresale/cachedfiles/pending.html @@ -0,0 +1,34 @@ +{% load compress %} +{% load i18n %} +{% load staticfiles %} + + + + {{ settings.PRETIX_INSTANCE_NAME }} + {% compress css %} + + {% endcompress %} + {% compress js %} + + {% endcompress %} + + + +
+ +

{% trans "We are preparing your file for download…" %}

+

+ {% trans "If this takes longer than a few minutes, please contact us." %} +

+
+ + + diff --git a/src/pretix/presale/urls.py b/src/pretix/presale/urls.py index dbc6481169..2676c08d00 100644 --- a/src/pretix/presale/urls.py +++ b/src/pretix/presale/urls.py @@ -1,5 +1,6 @@ from django.conf.urls import include, url +import pretix.presale.views.cachedfiles import pretix.presale.views.cart import pretix.presale.views.checkout import pretix.presale.views.event @@ -7,6 +8,8 @@ import pretix.presale.views.locale import pretix.presale.views.order urlpatterns = [ + url(r'^download/(?P[^/]+)/$', pretix.presale.views.cachedfiles.DownloadView.as_view(), + name='cachedfile.download'), url(r'^(?P[^/]+)/(?P[^/]+)/', include([ url(r'^$', pretix.presale.views.event.EventIndex.as_view(), name='event.index'), url(r'^cart/add$', pretix.presale.views.cart.CartAdd.as_view(), name='event.cart.add'), diff --git a/src/pretix/presale/views/cachedfiles.py b/src/pretix/presale/views/cachedfiles.py new file mode 100644 index 0000000000..e1d39c039d --- /dev/null +++ b/src/pretix/presale/views/cachedfiles.py @@ -0,0 +1,22 @@ +from django.http import HttpResponse +from django.shortcuts import get_object_or_404, redirect +from django.utils.functional import cached_property +from django.views.generic import TemplateView + +from pretix.base.models import CachedFile + + +class DownloadView(TemplateView): + template_name = "pretixpresale/cachedfiles/pending.html" + + @cached_property + def object(self): + return get_object_or_404(CachedFile, id=self.kwargs['id']) + + def get(self, request, *args, **kwargs): + if 'ajax' in request.GET: + return HttpResponse('1' if self.object.file else '0') + elif self.object.file: + return redirect(self.object.file.url) + else: + return super().get(request, *args, **kwargs) diff --git a/src/static/pretixpresale/less/cachedfiles.less b/src/static/pretixpresale/less/cachedfiles.less new file mode 100644 index 0000000000..2e816d8a86 --- /dev/null +++ b/src/static/pretixpresale/less/cachedfiles.less @@ -0,0 +1,20 @@ +@import "../../bootstrap/less/bootstrap.less"; +@import "../../fontawesome/less/font-awesome.less"; +@import "../../lightbox/css/lightbox.css"; + +@fa-font-path: "../../fontawesome/fonts"; + +@brand-primary: #8E44B3; + +body { + background: #ececec; + text-align: center; + padding: 50px 0; +} + +.big-animated-icon { + -webkit-animation: fa-spin 8s infinite linear; + animation: fa-spin 8s infinite linear; + font-size: 200px; + color: @brand-primary; +} diff --git a/src/tests/settings.py b/src/tests/settings.py index 0de54939d1..7aee6e4bbb 100644 --- a/src/tests/settings.py +++ b/src/tests/settings.py @@ -9,3 +9,5 @@ INSTALLED_APPS = INSTALLED_APPS + ( ) MEDIA_ROOT = os.path.join(TEST_DIR, 'media') + +EMAIL_BACKEND = 'django.core.mail.outbox'