Refs #44 -- Added background queue support for file export

This commit is contained in:
Raphael Michel
2015-09-15 22:56:02 +02:00
parent 0530416c66
commit 9ecd16c19c
17 changed files with 270 additions and 19 deletions
+1
View File
@@ -8,6 +8,7 @@ class PretixBaseConfig(AppConfig):
def ready(self): def ready(self):
from . import exporter # NOQA from . import exporter # NOQA
from . import payment # NOQA from . import payment # NOQA
from .services import export, mail # NOQA
try: try:
from .celery import app as celery_app # NOQA from .celery import app as celery_app # NOQA
+11 -7
View File
@@ -1,3 +1,7 @@
import decimal
import json
from django.core.serializers.json import DjangoJSONEncoder
from django.dispatch import receiver from django.dispatch import receiver
from django.http import HttpRequest, HttpResponse, JsonResponse from django.http import HttpRequest, HttpResponse, JsonResponse
@@ -58,13 +62,13 @@ class BaseExporter:
""" """
return {} 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 Render the exported file and return a tuple consisting of a filename, a file type
or redirects to it. and file content.
:type request: HttpRequest :type form_data: dict
:param request: The HTTP request of the user requesting the export :param form_data: The form data of the export details form
""" """
raise NotImplementedError() # NOQA raise NotImplementedError() # NOQA
@@ -73,7 +77,7 @@ class JSONExporter(BaseExporter):
identifier = 'json' identifier = 'json'
verbose_name = 'JSON' verbose_name = 'JSON'
def render(self, request): def render(self, form_data):
jo = { jo = {
'event': { 'event': {
'name': str(self.event.name), '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") @receiver(register_data_exporters, dispatch_uid="exporter_json")
@@ -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'),
),
]
@@ -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,
),
]
@@ -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),
),
]
@@ -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',
),
]
+22
View File
@@ -291,6 +291,22 @@ class User(AbstractBaseUser, PermissionsMixin):
return self.identifier # NOQA 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): class Organizer(Versionable):
""" """
This model represents an entity organizing events, e.g. a company, institution, This model represents an entity organizing events, e.g. a company, institution,
@@ -1687,6 +1703,12 @@ class Order(Versionable):
return True, quotas_locked 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): class QuestionAnswer(Versionable):
""" """
The answer to a Question, connected to an OrderPosition or CartPosition. The answer to a Question, connected to an OrderPosition or CartPosition.
+23
View File
@@ -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)
+1 -1
View File
@@ -82,7 +82,7 @@ def mail_send(to, subject, body, sender):
return False return False
if settings.HAS_CELERY: if settings.HAS_CELERY and settings.EMAIL_BACKEND != 'django.core.mail.outbox':
from pretix.celery import app from pretix.celery import app
mail_send_task = app.task(mail_send) mail_send_task = app.task(mail_send)
+9 -2
View File
@@ -1,3 +1,4 @@
from datetime import timedelta
from itertools import groupby from itertools import groupby
from django import forms from django import forms
@@ -11,7 +12,8 @@ from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views.generic import DetailView, ListView, TemplateView, View 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.orders import mark_order_paid
from pretix.base.services.stats import order_overview from pretix.base.services.stats import order_overview
from pretix.base.signals import ( 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.')) messages.error(self.request, _('There was a problem processing your input. See below for error details.'))
return self.get(*args, **kwargs) 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}))
+3 -8
View File
@@ -204,11 +204,6 @@ class OverviewReportExporter(BaseExporter):
identifier = 'pdfreport' identifier = 'pdfreport'
verbose_name = _('Order overview (PDF)') verbose_name = _('Order overview (PDF)')
def render(self, request): def render(self, form_data):
response = HttpResponse(content_type='application/pdf') report = OverviewReport(self.event)
response['Content-Disposition'] = 'inline; filename="report-%s.pdf"' % request.event.slug return 'report-%s.pdf' % self.event.slug, 'application/pdf', report.create()
report = OverviewReport(request.event)
response.write(report.create())
return response
+1 -1
View File
@@ -19,4 +19,4 @@ class EventMiddleware:
organizer__slug=url.kwargs['organizer'], organizer__slug=url.kwargs['organizer'],
).select_related('organizer')[0] ).select_related('organizer')[0]
except IndexError: except IndexError:
return HttpResponseNotFound() # TODO: Provide error message return HttpResponseNotFound('Unknown event') # TODO: Provide error message
@@ -0,0 +1,34 @@
{% load compress %}
{% load i18n %}
{% load staticfiles %}
<!DOCTYPE html>
<html>
<head>
<title>{{ settings.PRETIX_INSTANCE_NAME }}</title>
{% compress css %}
<link rel="stylesheet" type="text/less" href="{% static "pretixpresale/less/cachedfiles.less" %}" />
{% endcompress %}
{% compress js %}
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script>
{% endcompress %}
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div class="container">
<i class="fa fa-cog big-animated-icon"></i>
<h1>{% trans "We are preparing your file for download…" %}</h1>
<p>
{% trans "If this takes longer than a few minutes, please contact us." %}
</p>
</div>
<script type="text/javascript">
window.setInterval(function() {
$.get(location.href + '?ajax=1', function(data, status) {
if (data === "1") {
location.reload();
}
});
}, 500);
</script>
</body>
</html>
+3
View File
@@ -1,5 +1,6 @@
from django.conf.urls import include, url from django.conf.urls import include, url
import pretix.presale.views.cachedfiles
import pretix.presale.views.cart import pretix.presale.views.cart
import pretix.presale.views.checkout import pretix.presale.views.checkout
import pretix.presale.views.event import pretix.presale.views.event
@@ -7,6 +8,8 @@ import pretix.presale.views.locale
import pretix.presale.views.order import pretix.presale.views.order
urlpatterns = [ urlpatterns = [
url(r'^download/(?P<id>[^/]+)/$', pretix.presale.views.cachedfiles.DownloadView.as_view(),
name='cachedfile.download'),
url(r'^(?P<organizer>[^/]+)/(?P<event>[^/]+)/', include([ url(r'^(?P<organizer>[^/]+)/(?P<event>[^/]+)/', include([
url(r'^$', pretix.presale.views.event.EventIndex.as_view(), name='event.index'), 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'), url(r'^cart/add$', pretix.presale.views.cart.CartAdd.as_view(), name='event.cart.add'),
+22
View File
@@ -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)
@@ -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;
}
+2
View File
@@ -9,3 +9,5 @@ INSTALLED_APPS = INSTALLED_APPS + (
) )
MEDIA_ROOT = os.path.join(TEST_DIR, 'media') MEDIA_ROOT = os.path.join(TEST_DIR, 'media')
EMAIL_BACKEND = 'django.core.mail.outbox'