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

This commit is contained in:
Raphael Michel
2015-09-15 22:49:38 +02:00
parent 0530416c66
commit 9ecd16c19c
17 changed files with 270 additions and 19 deletions

View File

@@ -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

View File

@@ -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")

View File

@@ -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'),
),
]

View File

@@ -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,
),
]

View File

@@ -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),
),
]

View File

@@ -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',
),
]

View File

@@ -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.

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)

View File

@@ -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)

View File

@@ -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}))

View File

@@ -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()

View File

@@ -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

View File

@@ -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>

View File

@@ -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<id>[^/]+)/$', pretix.presale.views.cachedfiles.DownloadView.as_view(),
name='cachedfile.download'),
url(r'^(?P<organizer>[^/]+)/(?P<event>[^/]+)/', 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'),

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)

View File

@@ -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;
}

View File

@@ -9,3 +9,5 @@ INSTALLED_APPS = INSTALLED_APPS + (
)
MEDIA_ROOT = os.path.join(TEST_DIR, 'media')
EMAIL_BACKEND = 'django.core.mail.outbox'