mirror of
https://github.com/pretix/pretix.git
synced 2026-08-22 12:42:00 +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):
|
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
|
||||||
|
|||||||
@@ -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',
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
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)
|
||||||
|
|||||||
@@ -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}))
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
|
|||||||
Reference in New Issue
Block a user