Allow organizer-level exports with separate permission and no event selection

This commit is contained in:
Raphael Michel
2022-10-05 10:36:57 +02:00
parent 74e14285ee
commit 60cdfe4029
7 changed files with 96 additions and 21 deletions
+7 -1
View File
@@ -60,7 +60,13 @@ The exporter class
.. py:attribute:: BaseExporter.event .. py:attribute:: BaseExporter.event
The default constructor sets this property to the event we are currently The default constructor sets this property to the event we are currently
working for. working for. This will be ``None`` if the exporter is run for multiple
events.
.. py:attribute:: BaseExporter.events
The default constructor sets this property to the list of events to work
on, regardless of whether the exporter is called for one or multiple events.
.. autoattribute:: identifier .. autoattribute:: identifier
+15 -2
View File
@@ -35,7 +35,8 @@ from rest_framework.reverse import reverse
from pretix.api.serializers.exporters import ( from pretix.api.serializers.exporters import (
ExporterSerializer, JobRunSerializer, ExporterSerializer, JobRunSerializer,
) )
from pretix.base.models import CachedFile, Device, TeamAPIToken from pretix.base.exporter import OrganizerLevelExportMixin
from pretix.base.models import CachedFile, Device, Event, TeamAPIToken
from pretix.base.services.export import export, multiexport from pretix.base.services.export import export, multiexport
from pretix.base.signals import ( from pretix.base.signals import (
register_data_exporters, register_multievent_data_exporters, register_data_exporters, register_multievent_data_exporters,
@@ -155,7 +156,19 @@ class OrganizerExportersViewSet(ExportersMixin, viewsets.ViewSet):
organizer=self.request.organizer organizer=self.request.organizer
) )
responses = register_multievent_data_exporters.send(self.request.organizer) responses = register_multievent_data_exporters.send(self.request.organizer)
for ex in sorted([response(events, self.request.organizer) for r, response in responses if response], key=lambda ex: str(ex.verbose_name)): raw_exporters = [
response(Event.objects.none() if issubclass(response, OrganizerLevelExportMixin) else events, self.request.organizer)
for r, response in responses
if response
]
raw_exporters = [
ex for ex in raw_exporters
if (
not isinstance(ex, OrganizerLevelExportMixin) or
perm_holder.has_organizer_permission(self.request.organizer, ex.organizer_required_permission, self.request)
)
]
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
ex._serializer = JobRunSerializer(exporter=ex, events=events) ex._serializer = JobRunSerializer(exporter=ex, events=events)
exporters.append(ex) exporters.append(ex)
return exporters return exporters
+12 -2
View File
@@ -51,7 +51,7 @@ from pretix.helpers.safe_openpyxl import ( # NOQA: backwards compatibility for
SafeWorkbook, remove_invalid_excel_chars as excel_safe, SafeWorkbook, remove_invalid_excel_chars as excel_safe,
) )
__ = excel_safe # just so the compatbility import above is "used" and doesn't get removed by linter __ = excel_safe # just so the compatibility import above is "used" and doesn't get removed by linter
class BaseExporter: class BaseExporter:
@@ -80,7 +80,7 @@ class BaseExporter:
def verbose_name(self) -> str: def verbose_name(self) -> str:
""" """
A human-readable name for this exporter. This should be short but A human-readable name for this exporter. This should be short but
self-explaining. Good examples include 'JSON' or 'Microsoft Excel'. self-explaining. Good examples include 'Orders as JSON' or 'Orders as Microsoft Excel'.
""" """
raise NotImplementedError() # NOQA raise NotImplementedError() # NOQA
@@ -137,6 +137,16 @@ class BaseExporter:
raise NotImplementedError() # NOQA raise NotImplementedError() # NOQA
class OrganizerLevelExportMixin:
@property
def organizer_required_permission(self) -> str:
"""
The permission level required to use this exporter. Only useful for organizer-level exports,
not for event-level exports.
"""
return 'can_view_orders'
class ListExporter(BaseExporter): class ListExporter(BaseExporter):
ProgressSetTotal = namedtuple('ProgressSetTotal', 'total') ProgressSetTotal = namedtuple('ProgressSetTotal', 'total')
+7 -3
View File
@@ -60,7 +60,9 @@ from pretix.base.settings import PERSON_NAME_SCHEMES
from ...control.forms.filter import get_all_payment_providers from ...control.forms.filter import get_all_payment_providers
from ...helpers import GroupConcat from ...helpers import GroupConcat
from ...helpers.iter import chunked_iterable from ...helpers.iter import chunked_iterable
from ..exporter import ListExporter, MultiSheetListExporter from ..exporter import (
ListExporter, MultiSheetListExporter, OrganizerLevelExportMixin,
)
from ..signals import ( from ..signals import (
register_data_exporters, register_multievent_data_exporters, register_data_exporters, register_multievent_data_exporters,
) )
@@ -884,9 +886,10 @@ class QuotaListExporter(ListExporter):
return '{}_quotas'.format(self.event.slug) return '{}_quotas'.format(self.event.slug)
class GiftcardTransactionListExporter(ListExporter): class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
identifier = 'giftcardtransactionlist' identifier = 'giftcardtransactionlist'
verbose_name = gettext_lazy('Gift card transactions') verbose_name = gettext_lazy('Gift card transactions')
organizer_required_permission = 'can_manage_gift_cards'
@property @property
def additional_form_fields(self): def additional_form_fields(self):
@@ -998,9 +1001,10 @@ class GiftcardRedemptionListExporter(ListExporter):
return '{}_giftcardredemptions'.format(self.event.slug) return '{}_giftcardredemptions'.format(self.event.slug)
class GiftcardListExporter(ListExporter): class GiftcardListExporter(OrganizerLevelExportMixin, ListExporter):
identifier = 'giftcardlist' identifier = 'giftcardlist'
verbose_name = gettext_lazy('Gift cards') verbose_name = gettext_lazy('Gift cards')
organizer_required_permission = 'can_manage_gift_cards'
@property @property
def additional_form_fields(self): def additional_form_fields(self):
+12
View File
@@ -26,6 +26,7 @@ from django.core.files.base import ContentFile
from django.utils.timezone import override from django.utils.timezone import override
from django.utils.translation import gettext from django.utils.translation import gettext
from pretix.base.exporter import OrganizerLevelExportMixin
from pretix.base.i18n import LazyLocaleException, language from pretix.base.i18n import LazyLocaleException, language
from pretix.base.models import ( from pretix.base.models import (
CachedFile, Device, Event, Organizer, TeamAPIToken, User, cachedfile_name, CachedFile, Device, Event, Organizer, TeamAPIToken, User, cachedfile_name,
@@ -119,6 +120,17 @@ def multiexport(self, organizer: Organizer, user: User, device: int, token: int,
continue continue
ex = response(events, organizer, set_progress) ex = response(events, organizer, set_progress)
if ex.identifier == provider: if ex.identifier == provider:
if (
isinstance(ex, OrganizerLevelExportMixin) and
not staff_session and
not (device or token or user).has_organizer_permission(self.request.organizer,
ex.organizer_required_permission,
self.request)
):
raise ExportError(
gettext('You do not have sufficient permission to perform this export.')
)
d = ex.render(form_data) d = ex.render(form_data)
if d is None: if d is None:
raise ExportError( raise ExportError(
+27 -13
View File
@@ -65,6 +65,7 @@ from pretix.api.models import WebHook
from pretix.api.webhooks import manually_retry_all_calls from pretix.api.webhooks import manually_retry_all_calls
from pretix.base.auth import get_auth_backends from pretix.base.auth import get_auth_backends
from pretix.base.channels import get_all_sales_channels from pretix.base.channels import get_all_sales_channels
from pretix.base.exporter import OrganizerLevelExportMixin
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.models import ( from pretix.base.models import (
CachedFile, Customer, Device, Gate, GiftCard, Invoice, LogEntry, CachedFile, Customer, Device, Gate, GiftCard, Invoice, LogEntry,
@@ -1508,7 +1509,19 @@ class ExportMixin:
) )
responses = register_multievent_data_exporters.send(self.request.organizer) responses = register_multievent_data_exporters.send(self.request.organizer)
id = self.request.GET.get("identifier") or self.request.POST.get("exporter") id = self.request.GET.get("identifier") or self.request.POST.get("exporter")
for ex in sorted([response(events, self.request.organizer) for r, response in responses if response], key=lambda ex: str(ex.verbose_name)): raw_exporters = [
response(Event.objects.none() if issubclass(response, OrganizerLevelExportMixin) else events, self.request.organizer)
for r, response in responses
if response
]
raw_exporters = [
ex for ex in raw_exporters
if (
not isinstance(ex, OrganizerLevelExportMixin) or
self.request.user.has_organizer_permission(self.request.organizer, ex.organizer_required_permission, self.request)
)
]
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
if id and ex.identifier != id: if id and ex.identifier != id:
continue continue
@@ -1526,18 +1539,19 @@ class ExportMixin:
initial=initial initial=initial
) )
ex.form.fields = ex.export_form_fields ex.form.fields = ex.export_form_fields
ex.form.fields.update([ if not isinstance(ex, OrganizerLevelExportMixin):
('events', ex.form.fields.update([
forms.ModelMultipleChoiceField( ('events',
queryset=events, forms.ModelMultipleChoiceField(
initial=events, queryset=events,
widget=forms.CheckboxSelectMultiple( initial=events,
attrs={'class': 'scrolling-multiple-choice'} widget=forms.CheckboxSelectMultiple(
), attrs={'class': 'scrolling-multiple-choice'}
label=_('Events'), ),
required=True label=_('Events'),
)), required=True
]) )),
])
exporters.append(ex) exporters.append(ex)
return exporters return exporters
+16
View File
@@ -191,3 +191,19 @@ def test_gone_without_celery(token_client, organizer, team, event):
cf = CachedFile.objects.create() cf = CachedFile.objects.create()
resp = token_client.get('/api/v1/organizers/{}/events/{}/exporters/orderlist/download/{}/{}/'.format(organizer.slug, event.slug, uuid.uuid4(), cf.id)) resp = token_client.get('/api/v1/organizers/{}/events/{}/exporters/orderlist/download/{}/{}/'.format(organizer.slug, event.slug, uuid.uuid4(), cf.id))
assert resp.status_code == 410 assert resp.status_code == 410
@pytest.mark.django_db
def test_org_level_export(token_client, organizer, team, event):
resp = token_client.post('/api/v1/organizers/{}/exporters/giftcardlist/run/'.format(organizer.slug), data={
'_format': 'xlsx',
}, format='json')
assert resp.status_code == 202
team.can_manage_gift_cards = False
team.save()
resp = token_client.post('/api/v1/organizers/{}/exporters/giftcardlist/run/'.format(organizer.slug), data={
'_format': 'xlsx',
}, format='json')
assert resp.status_code == 404