mirror of
https://github.com/pretix/pretix.git
synced 2026-01-26 00:52:26 +00:00
Refs #44 -- Added background queue support for file export
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
59
src/pretix/base/migrations/0009_auto_20150915_2003.py
Normal file
59
src/pretix/base/migrations/0009_auto_20150915_2003.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
20
src/pretix/base/migrations/0010_cachedfile_type.py
Normal file
20
src/pretix/base/migrations/0010_cachedfile_type.py
Normal 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,
|
||||
),
|
||||
]
|
||||
21
src/pretix/base/migrations/0011_auto_20150915_2020.py
Normal file
21
src/pretix/base/migrations/0011_auto_20150915_2020.py
Normal 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),
|
||||
),
|
||||
]
|
||||
18
src/pretix/base/migrations/0012_remove_order_tickets.py
Normal file
18
src/pretix/base/migrations/0012_remove_order_tickets.py
Normal 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',
|
||||
),
|
||||
]
|
||||
@@ -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.
|
||||
|
||||
23
src/pretix/base/services/export.py
Normal file
23
src/pretix/base/services/export.py
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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}))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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'),
|
||||
|
||||
22
src/pretix/presale/views/cachedfiles.py
Normal file
22
src/pretix/presale/views/cachedfiles.py
Normal 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)
|
||||
20
src/static/pretixpresale/less/cachedfiles.less
Normal file
20
src/static/pretixpresale/less/cachedfiles.less
Normal 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;
|
||||
}
|
||||
@@ -9,3 +9,5 @@ INSTALLED_APPS = INSTALLED_APPS + (
|
||||
)
|
||||
|
||||
MEDIA_ROOT = os.path.join(TEST_DIR, 'media')
|
||||
|
||||
EMAIL_BACKEND = 'django.core.mail.outbox'
|
||||
|
||||
Reference in New Issue
Block a user