diff --git a/doc/development/api/exporter.rst b/doc/development/api/exporter.rst index c45c76d08..2228aaa0c 100644 --- a/doc/development/api/exporter.rst +++ b/doc/development/api/exporter.rst @@ -29,6 +29,22 @@ that we'll provide in this plugin:: from .exporter import MyExporter return MyExporter +Some exporters might also prove to be useful, when provided on an organizer-level. In order to declare your +exporter as capable of providing exports spanning multiple events, your plugin should listen for this signal +and return the subclass of ``pretix.base.exporter.BaseExporter`` that we'll provide in this plugin:: + + from django.dispatch import receiver + + from pretix.base.signals import register_multievent_data_exporters + + + @receiver(register_multievent_data_exporters, dispatch_uid="multieventexporter_myexporter") + def register_multievent_data_exporter(sender, **kwargs): + from .exporter import MyExporter + return MyExporter + +If your exporter supports both event-level and multi-event level exports, you will need to listen for both +signals. The exporter class ------------------ diff --git a/src/pretix/base/exporter.py b/src/pretix/base/exporter.py index ee8ffc7c2..dfe131857 100644 --- a/src/pretix/base/exporter.py +++ b/src/pretix/base/exporter.py @@ -6,11 +6,14 @@ from typing import Tuple from defusedcsv import csv from django import forms +from django.db.models import QuerySet from django.utils.formats import localize from django.utils.translation import gettext, gettext_lazy as _ from openpyxl import Workbook from openpyxl.cell.cell import KNOWN_TYPES +from pretix.base.models import Event + class BaseExporter: """ @@ -19,6 +22,12 @@ class BaseExporter: def __init__(self, event): self.event = event + self.is_multievent = isinstance(event, QuerySet) + if isinstance(event, QuerySet): + self.events = event + self.event = None + else: + self.events = Event.objects.filter(pk=event.pk) def __str__(self): return self.identifier diff --git a/src/pretix/base/exporters/invoices.py b/src/pretix/base/exporters/invoices.py index 201981e78..d6a41d7bb 100644 --- a/src/pretix/base/exporters/invoices.py +++ b/src/pretix/base/exporters/invoices.py @@ -9,11 +9,14 @@ from django.db.models import Exists, OuterRef, Q from django.dispatch import receiver from django.utils.translation import gettext_lazy as _ -from pretix.base.models import OrderPayment +from pretix.base.models import Invoice, OrderPayment +from ...control.forms.filter import get_all_payment_providers from ..exporter import BaseExporter from ..services.invoices import invoice_pdf_task -from ..signals import register_data_exporters +from ..signals import ( + register_data_exporters, register_multievent_data_exporters, +) class InvoiceExporter(BaseExporter): @@ -21,7 +24,7 @@ class InvoiceExporter(BaseExporter): verbose_name = _('All invoices') def render(self, form_data: dict, output_file=None): - qs = self.event.invoices.filter(shredded=False) + qs = Invoice.objects.filter(event__in=self.events, shredded=False) if form_data.get('payment_provider'): qs = qs.annotate( @@ -68,11 +71,16 @@ class InvoiceExporter(BaseExporter): if not any: return None + if self.is_multievent: + filename = '{}_invoices.zip'.format(self.events.first().organizer.slug) + else: + filename = '{}_invoices'.format(self.event.slug) + if output_file: - return '{}_invoices.zip'.format(self.event.slug), 'application/zip', None + return filename, 'application/zip', None else: with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf: - return '{}_invoices.zip'.format(self.event.slug), 'application/zip', zipf.read() + return filename, 'application/zip', zipf.read() @property def export_form_fields(self): @@ -99,7 +107,7 @@ class InvoiceExporter(BaseExporter): label=_('Payment provider'), choices=[ ('', _('All payment providers')), - ] + [ + ] + get_all_payment_providers() if self.is_multievent else [ (k, v.verbose_name) for k, v in self.event.get_payment_providers().items() ], required=False, @@ -115,3 +123,8 @@ class InvoiceExporter(BaseExporter): @receiver(register_data_exporters, dispatch_uid="exporter_invoices") def register_invoice_export(sender, **kwargs): return InvoiceExporter + + +@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_invoices") +def register_multievent_invoice_export(sender, **kwargs): + return InvoiceExporter diff --git a/src/pretix/base/exporters/mail.py b/src/pretix/base/exporters/mail.py index cb6b84fd9..912821bd2 100644 --- a/src/pretix/base/exporters/mail.py +++ b/src/pretix/base/exporters/mail.py @@ -8,7 +8,9 @@ from pretix.base.models import OrderPosition from ..exporter import BaseExporter from ..models import Order -from ..signals import register_data_exporters +from ..signals import ( + register_data_exporters, register_multievent_data_exporters, +) class MailExporter(BaseExporter): @@ -16,14 +18,18 @@ class MailExporter(BaseExporter): verbose_name = _('Email addresses (text file)') def render(self, form_data: dict): - qs = self.event.orders.filter(status__in=form_data['status']) + qs = Order.objects.filter(event__in=self.events, status__in=form_data['status']).prefetch_related('event') addrs = qs.values('email') pos = OrderPosition.objects.filter( - order__event=self.event, order__status__in=form_data['status'] + order__event__in=self.events, order__status__in=form_data['status'] ).values('attendee_email') data = "\r\n".join(set(a['email'] for a in addrs if a['email']) | set(a['attendee_email'] for a in pos if a['attendee_email'])) - return '{}_pretixemails.txt'.format(self.event.slug), 'text/plain', data.encode("utf-8") + + if self.is_multievent: + return '{}_pretixemails.txt'.format(self.events.first().organizer.slug), 'text/plain', data.encode("utf-8") + else: + return '{}_pretixemails.txt'.format(self.event.slug), 'text/plain', data.encode("utf-8") @property def export_form_fields(self): @@ -44,3 +50,8 @@ class MailExporter(BaseExporter): @receiver(register_data_exporters, dispatch_uid="exporter_mail") def register_mail_export(sender, **kwargs): return MailExporter + + +@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_mail") +def register_multievent_mail_export(sender, **kwargs): + return MailExporter diff --git a/src/pretix/base/exporters/orderlist.py b/src/pretix/base/exporters/orderlist.py index 11a6e0837..bffcbee85 100644 --- a/src/pretix/base/exporters/orderlist.py +++ b/src/pretix/base/exporters/orderlist.py @@ -11,14 +11,18 @@ from django.utils.formats import date_format from django.utils.translation import gettext as _, gettext_lazy, pgettext from pretix.base.models import ( - GiftCard, InvoiceAddress, InvoiceLine, Order, OrderPosition, Question, + GiftCard, Invoice, InvoiceAddress, InvoiceLine, Order, OrderPosition, + Question, ) from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund from pretix.base.services.quotas import QuotaAvailability from pretix.base.settings import PERSON_NAME_SCHEMES +from ...control.forms.filter import get_all_payment_providers from ..exporter import ListExporter, MultiSheetListExporter -from ..signals import register_data_exporters +from ..signals import ( + register_data_exporters, register_multievent_data_exporters, +) class OrderListExporter(MultiSheetListExporter): @@ -50,13 +54,13 @@ class OrderListExporter(MultiSheetListExporter): tax_rates = set( a for a in OrderFee.objects.filter( - order__event=self.event + order__event__in=self.events ).values_list('tax_rate', flat=True).distinct().order_by() ) tax_rates |= set( a for a in OrderPosition.objects.filter( - order__event=self.event + order__event__in=self.events ).values_list('tax_rate', flat=True).distinct().order_by() ) tax_rates = sorted(tax_rates) @@ -71,8 +75,6 @@ class OrderListExporter(MultiSheetListExporter): return self.iterate_fees(form_data) def iterate_orders(self, form_data: dict): - tz = pytz.timezone(self.event.settings.timezone) - p_date = OrderPayment.objects.filter( order=OuterRef('pk'), state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED), @@ -86,20 +88,20 @@ class OrderListExporter(MultiSheetListExporter): s = OrderPosition.objects.filter( order=OuterRef('pk') ).order_by().values('order').annotate(k=Count('id')).values('k') - qs = self.event.orders.annotate( + qs = Order.objects.filter(event__in=self.events).annotate( payment_date=Subquery(p_date, output_field=DateTimeField()), pcnt=Subquery(s, output_field=IntegerField()) - ).select_related('invoice_address').prefetch_related('invoices') + ).select_related('invoice_address').prefetch_related('invoices').prefetch_related('event') if form_data['paid_only']: qs = qs.filter(status=Order.STATUS_PAID) tax_rates = self._get_all_tax_rates(qs) headers = [ - _('Order code'), _('Order total'), _('Status'), _('Email'), _('Order date'), + _('Event slug'), _('Order code'), _('Order total'), _('Status'), _('Email'), _('Order date'), _('Company'), _('Name'), ] - name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme] - if len(name_scheme['fields']) > 1: + name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme] if not self.is_multievent else None + if name_scheme and len(name_scheme['fields']) > 1: for k, label, w in name_scheme['fields']: headers.append(label) headers += [ @@ -140,7 +142,10 @@ class OrderListExporter(MultiSheetListExporter): } for order in qs.order_by('datetime'): + tz = pytz.timezone(order.event.settings.timezone) + row = [ + order.event.slug, order.code, order.total, order.get_status_display(), @@ -152,7 +157,7 @@ class OrderListExporter(MultiSheetListExporter): order.invoice_address.company, order.invoice_address.name, ] - if len(name_scheme['fields']) > 1: + if name_scheme and len(name_scheme['fields']) > 1: for k, label, w in name_scheme['fields']: row.append( order.invoice_address.name_parts.get(k, '') @@ -167,7 +172,7 @@ class OrderListExporter(MultiSheetListExporter): order.invoice_address.vat_id, ] except InvoiceAddress.DoesNotExist: - row += [''] * (8 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0)) + row += [''] * (8 + (len(name_scheme['fields']) if name_scheme and len(name_scheme['fields']) > 1 else 0)) row += [ order.payment_date.astimezone(tz).strftime('%Y-%m-%d') if order.payment_date else '', @@ -197,15 +202,14 @@ class OrderListExporter(MultiSheetListExporter): yield row def iterate_fees(self, form_data: dict): - tz = pytz.timezone(self.event.settings.timezone) - qs = OrderFee.objects.filter( - order__event=self.event, + order__event__in=self.events, ).select_related('order', 'order__invoice_address', 'tax_rule') if form_data['paid_only']: qs = qs.filter(order__status=Order.STATUS_PAID) headers = [ + _('Event slug'), _('Order code'), _('Status'), _('Email'), @@ -219,8 +223,8 @@ class OrderListExporter(MultiSheetListExporter): _('Company'), _('Invoice address name'), ] - name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme] - if len(name_scheme['fields']) > 1: + name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme] if not self.is_multievent else None + if name_scheme and len(name_scheme['fields']) > 1: for k, label, w in name_scheme['fields']: headers.append(_('Invoice address name') + ': ' + str(label)) headers += [ @@ -231,7 +235,9 @@ class OrderListExporter(MultiSheetListExporter): for op in qs.order_by('order__datetime'): order = op.order + tz = pytz.timezone(order.event.settings.timezone) row = [ + order.event.slug, order.code, order.get_status_display(), order.email, @@ -248,7 +254,7 @@ class OrderListExporter(MultiSheetListExporter): order.invoice_address.company, order.invoice_address.name, ] - if len(name_scheme['fields']) > 1: + if name_scheme and len(name_scheme['fields']) > 1: for k, label, w in name_scheme['fields']: row.append( order.invoice_address.name_parts.get(k, '') @@ -263,14 +269,12 @@ class OrderListExporter(MultiSheetListExporter): order.invoice_address.vat_id, ] except InvoiceAddress.DoesNotExist: - row += [''] * (8 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0)) + row += [''] * (8 + (len(name_scheme['fields']) if name_scheme and len(name_scheme['fields']) > 1 else 0)) yield row def iterate_positions(self, form_data: dict): - tz = pytz.timezone(self.event.settings.timezone) - qs = OrderPosition.objects.filter( - order__event=self.event, + order__event__in=self.events, ).select_related( 'order', 'order__invoice_address', 'item', 'variation', 'voucher', 'tax_rule' @@ -281,13 +285,14 @@ class OrderListExporter(MultiSheetListExporter): qs = qs.filter(order__status=Order.STATUS_PAID) headers = [ + _('Event slug'), _('Order code'), _('Position ID'), _('Status'), _('Email'), _('Order date'), ] - if self.event.has_subevents: + if self.events.filter(has_subevents=True).exists(): headers.append(pgettext('subevent', 'Date')) headers.append(_('Start date')) headers.append(_('End date')) @@ -300,8 +305,8 @@ class OrderListExporter(MultiSheetListExporter): _('Tax value'), _('Attendee name'), ] - name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme] - if len(name_scheme['fields']) > 1: + name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme] if not self.is_multievent else None + if name_scheme and len(name_scheme['fields']) > 1: for k, label, w in name_scheme['fields']: headers.append(_('Attendee name') + ': ' + str(label)) headers += [ @@ -315,7 +320,8 @@ class OrderListExporter(MultiSheetListExporter): _('Voucher'), _('Pseudonymization ID'), ] - questions = list(self.event.questions.all()) + + questions = list(Question.objects.filter(event__in=self.events)) options = {} for q in questions: if q.type == Question.TYPE_CHOICE_MULTIPLE: @@ -329,7 +335,7 @@ class OrderListExporter(MultiSheetListExporter): _('Company'), _('Invoice address name'), ] - if len(name_scheme['fields']) > 1: + if name_scheme and len(name_scheme['fields']) > 1: for k, label, w in name_scheme['fields']: headers.append(_('Invoice address name') + ': ' + str(label)) headers += [ @@ -343,18 +349,20 @@ class OrderListExporter(MultiSheetListExporter): for op in qs.order_by('order__datetime', 'positionid'): order = op.order + tz = pytz.timezone(order.event.settings.timezone) row = [ + order.event.slug, order.code, op.positionid, order.get_status_display(), order.email, order.datetime.astimezone(tz).strftime('%Y-%m-%d'), ] - if self.event.has_subevents: + if order.event.has_subevents: row.append(op.subevent.name) - row.append(op.subevent.date_from.astimezone(self.event.timezone).strftime('%Y-%m-%d %H:%M:%S')) + row.append(op.subevent.date_from.astimezone(order.event.timezone).strftime('%Y-%m-%d %H:%M:%S')) if op.subevent.date_to: - row.append(op.subevent.date_to.astimezone(self.event.timezone).strftime('%Y-%m-%d %H:%M:%S')) + row.append(op.subevent.date_to.astimezone(order.event.timezone).strftime('%Y-%m-%d %H:%M:%S')) else: row.append('') row += [ @@ -366,7 +374,7 @@ class OrderListExporter(MultiSheetListExporter): op.tax_value, op.attendee_name, ] - if len(name_scheme['fields']) > 1: + if name_scheme and len(name_scheme['fields']) > 1: for k, label, w in name_scheme['fields']: row.append( op.attendee_name_parts.get(k, '') @@ -404,7 +412,7 @@ class OrderListExporter(MultiSheetListExporter): order.invoice_address.company, order.invoice_address.name, ] - if len(name_scheme['fields']) > 1: + if name_scheme and len(name_scheme['fields']) > 1: for k, label, w in name_scheme['fields']: row.append( order.invoice_address.name_parts.get(k, '') @@ -419,7 +427,7 @@ class OrderListExporter(MultiSheetListExporter): order.invoice_address.vat_id, ] except InvoiceAddress.DoesNotExist: - row += [''] * (8 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0)) + row += [''] * (8 + (len(name_scheme['fields']) if name_scheme and len(name_scheme['fields']) > 1 else 0)) row += [ order.sales_channel, order.locale @@ -427,7 +435,10 @@ class OrderListExporter(MultiSheetListExporter): yield row def get_filename(self): - return '{}_orders'.format(self.event.slug) + if self.is_multievent: + return '{}_orders'.format(self.events.first().organizer.slug) + else: + return '{}_orders'.format(self.event.slug) class PaymentListExporter(ListExporter): @@ -459,31 +470,27 @@ class PaymentListExporter(ListExporter): ) def iterate_list(self, form_data): - tz = pytz.timezone(self.event.settings.timezone) - - provider_names = { - k: v.verbose_name - for k, v in self.event.get_payment_providers().items() - } + provider_names = dict(get_all_payment_providers()) payments = OrderPayment.objects.filter( - order__event=self.event, + order__event__in=self.events, state__in=form_data.get('payment_states', []) ).order_by('created') refunds = OrderRefund.objects.filter( - order__event=self.event, + order__event__in=self.events, state__in=form_data.get('refund_states', []) ).order_by('created') objs = sorted(list(payments) + list(refunds), key=lambda o: o.created) headers = [ - _('Order'), _('Payment ID'), _('Creation date'), _('Completion date'), _('Status'), + _('Event slug'), _('Order'), _('Payment ID'), _('Creation date'), _('Completion date'), _('Status'), _('Status code'), _('Amount'), _('Payment method') ] yield headers for obj in objs: + tz = pytz.timezone(obj.order.event.settings.timezone) if isinstance(obj, OrderPayment) and obj.payment_date: d2 = obj.payment_date.astimezone(tz).date().strftime('%Y-%m-%d') elif isinstance(obj, OrderRefund) and obj.execution_date: @@ -491,6 +498,7 @@ class PaymentListExporter(ListExporter): else: d2 = '' row = [ + obj.order.event.slug, obj.order.code, obj.full_id, obj.created.astimezone(tz).date().strftime('%Y-%m-%d'), @@ -503,7 +511,10 @@ class PaymentListExporter(ListExporter): yield row def get_filename(self): - return '{}_payments'.format(self.event.slug) + if self.is_multievent: + return '{}_payments'.format(self.events.first().organizer.slug) + else: + return '{}_payments'.format(self.event.slug) class QuotaListExporter(ListExporter): @@ -585,7 +596,7 @@ class InvoiceDataExporter(MultiSheetListExporter): _('Total value (without taxes)'), _('Payment matching IDs'), ] - qs = self.event.invoices.order_by('full_invoice_no').select_related( + qs = Invoice.objects.filter(event__in=self.events).order_by('full_invoice_no').select_related( 'order', 'refers' ).prefetch_related('order__payments').annotate( total_gross=Subquery( @@ -682,7 +693,7 @@ class InvoiceDataExporter(MultiSheetListExporter): _('Invoice recipient:') + ' ' + _('Internal reference'), ] qs = InvoiceLine.objects.filter( - invoice__event=self.event + invoice__event__in=self.events ).order_by('invoice__full_invoice_no', 'position').select_related( 'invoice', 'invoice__order', 'invoice__refers' ) @@ -723,7 +734,10 @@ class InvoiceDataExporter(MultiSheetListExporter): ] def get_filename(self): - return '{}_invoices'.format(self.event.slug) + if self.is_multievent: + return '{}_invoices'.format(self.events.first().organizer.slug) + else: + return '{}_invoices'.format(self.event.slug) class GiftcardRedemptionListExporter(ListExporter): @@ -731,27 +745,27 @@ class GiftcardRedemptionListExporter(ListExporter): verbose_name = gettext_lazy('Gift card redemptions') def iterate_list(self, form_data): - tz = pytz.timezone(self.event.settings.timezone) - payments = OrderPayment.objects.filter( - order__event=self.event, + order__event__in=self.events, provider='giftcard' ).order_by('created') refunds = OrderRefund.objects.filter( - order__event=self.event, + order__event__in=self.events, provider='giftcard' ).order_by('created') objs = sorted(list(payments) + list(refunds), key=lambda o: (o.order.code, o.created)) headers = [ - _('Order'), _('Payment ID'), _('Date'), _('Gift card code'), _('Amount'), _('Issuer') + _('Event slug'), _('Order'), _('Payment ID'), _('Date'), _('Gift card code'), _('Amount'), _('Issuer') ] yield headers for obj in objs: + tz = pytz.timezone(obj.order.event.settings.timezone) gc = GiftCard.objects.get(pk=obj.info_data.get('gift_card')) row = [ + obj.order.event.slug, obj.order.code, obj.full_id, obj.created.astimezone(tz).date().strftime('%Y-%m-%d'), @@ -762,7 +776,10 @@ class GiftcardRedemptionListExporter(ListExporter): yield row def get_filename(self): - return '{}_giftcardredemptions'.format(self.event.slug) + if self.is_multievent: + return '{}_giftcardredemptions'.format(self.events.first().organizer.slug) + else: + return '{}_giftcardredemptions'.format(self.event.slug) @receiver(register_data_exporters, dispatch_uid="exporter_orderlist") @@ -770,11 +787,21 @@ def register_orderlist_exporter(sender, **kwargs): return OrderListExporter +@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_orderlist") +def register_multievent_orderlist_exporter(sender, **kwargs): + return OrderListExporter + + @receiver(register_data_exporters, dispatch_uid="exporter_paymentlist") def register_paymentlist_exporter(sender, **kwargs): return PaymentListExporter +@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_paymentlist") +def register_multievent_paymentlist_exporter(sender, **kwargs): + return PaymentListExporter + + @receiver(register_data_exporters, dispatch_uid="exporter_quotalist") def register_quotalist_exporter(sender, **kwargs): return QuotaListExporter @@ -785,6 +812,16 @@ def register_invoicedata_exporter(sender, **kwargs): return InvoiceDataExporter +@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_invoicedata") +def register_multievent_invoicedatae_xporter(sender, **kwargs): + return InvoiceDataExporter + + @receiver(register_data_exporters, dispatch_uid="exporter_giftcardredemptionlist") def register_giftcardredemptionlist_exporter(sender, **kwargs): return GiftcardRedemptionListExporter + + +@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_giftcardredemptionlist") +def register_multievent_i_giftcardredemptionlist_exporter(sender, **kwargs): + return GiftcardRedemptionListExporter diff --git a/src/pretix/base/services/export.py b/src/pretix/base/services/export.py index 47caaba5f..60f72bce0 100644 --- a/src/pretix/base/services/export.py +++ b/src/pretix/base/services/export.py @@ -5,9 +5,15 @@ from django.utils.timezone import override from django.utils.translation import gettext from pretix.base.i18n import LazyLocaleException, language -from pretix.base.models import CachedFile, Event, cachedfile_name -from pretix.base.services.tasks import ProfiledEventTask -from pretix.base.signals import register_data_exporters +from pretix.base.models import ( + CachedFile, Event, Organizer, User, cachedfile_name, +) +from pretix.base.services.tasks import ( + ProfiledEventTask, ProfiledOrganizerUserTask, +) +from pretix.base.signals import ( + register_data_exporters, register_multievent_data_exporters, +) from pretix.celery_app import app @@ -32,3 +38,26 @@ def export(event: Event, fileid: str, provider: str, form_data: Dict[str, Any]) file.file.save(cachedfile_name(file, file.filename), ContentFile(data)) file.save() return file.pk + + +@app.task(base=ProfiledOrganizerUserTask, throws=(ExportError,)) +def multiexport(organizer: Organizer, user: User, fileid: str, provider: str, form_data: Dict[str, Any]) -> None: + file = CachedFile.objects.get(id=fileid) + with language(user.locale), override(user.timezone): + allowed_events = user.get_events_with_permission('can_view_orders') + + events = allowed_events.filter(pk__in=form_data.get('events')) + responses = register_multievent_data_exporters.send(organizer) + + for receiver, response in responses: + ex = response(events) + if ex.identifier == provider: + d = ex.render(form_data) + if d is None: + raise ExportError( + gettext('Your export did not contain any data.') + ) + file.filename, file.type, data = d + file.file.save(cachedfile_name(file, file.filename), ContentFile(data)) + file.save() + return file.pk diff --git a/src/pretix/base/services/tasks.py b/src/pretix/base/services/tasks.py index 08efc47f7..ee5751cf2 100644 --- a/src/pretix/base/services/tasks.py +++ b/src/pretix/base/services/tasks.py @@ -19,7 +19,7 @@ from django_scopes import scope, scopes_disabled from pretix.base.metrics import ( pretix_task_duration_seconds, pretix_task_runs_total, ) -from pretix.base.models import Event +from pretix.base.models import Event, Organizer, User from pretix.celery_app import app @@ -88,10 +88,30 @@ class EventTask(app.Task): return ret +class OrganizerUserTask(app.Task): + def __call__(self, *args, **kwargs): + organizer_id = kwargs['organizer'] + with scopes_disabled(): + organizer = Organizer.objects.get(pk=organizer_id) + kwargs['organizer'] = organizer + + user_id = kwargs['user'] + user = User.objects.get(pk=user_id) + kwargs['user'] = user + + with scope(organizer=organizer): + ret = super().__call__(*args, **kwargs) + return ret + + class ProfiledEventTask(ProfiledTask, EventTask): pass +class ProfiledOrganizerUserTask(ProfiledTask, OrganizerUserTask): + pass + + class TransactionAwareTask(ProfiledTask): """ Task class which is aware of django db transactions and only executes tasks diff --git a/src/pretix/base/signals.py b/src/pretix/base/signals.py index cdae79e8a..858789b27 100644 --- a/src/pretix/base/signals.py +++ b/src/pretix/base/signals.py @@ -268,6 +268,16 @@ subclass of pretix.base.exporter.BaseExporter As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ +register_multievent_data_exporters = django.dispatch.Signal( + providing_args=["event"] +) +""" +This signal is sent out to get all known data exporters, which support exporting data for +multiple events. Receivers should return a subclass of pretix.base.exporter.BaseExporter + +The ``sender`` keyword argument will contain an organizer. +""" + validate_order = EventPluginSignal( providing_args=["payment_provider", "positions", "email", "locale", "invoice_address", "meta_info"] diff --git a/src/pretix/control/navigation.py b/src/pretix/control/navigation.py index 0675c68e9..aae10a117 100644 --- a/src/pretix/control/navigation.py +++ b/src/pretix/control/navigation.py @@ -411,6 +411,14 @@ def get_organizer_navigation(request): 'active': url.url_name == 'organizer', 'icon': 'calendar', }, + { + 'label': _('Export'), + 'url': reverse('control:organizer.export', kwargs={ + 'organizer': request.organizer.slug, + }), + 'active': 'organizer.export' in url.url_name, + 'icon': 'download', + }, ] if 'can_change_organizer_settings' in request.orgapermset: nav.append({ diff --git a/src/pretix/control/templates/pretixcontrol/organizers/export.html b/src/pretix/control/templates/pretixcontrol/organizers/export.html new file mode 100644 index 000000000..f02715dc6 --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/organizers/export.html @@ -0,0 +1,37 @@ +{% extends "pretixcontrol/event/base.html" %} +{% load i18n %} +{% load bootstrap3 %} +{% load order_overview %} +{% block title %}{% trans "Data export" %}{% endblock %} +{% block content %} +