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

This commit is contained in:
Raphael Michel
2022-10-05 09:56:43 +02:00
parent 74e14285ee
commit 60cdfe4029
7 changed files with 96 additions and 21 deletions

View File

@@ -60,7 +60,13 @@ The exporter class
.. py:attribute:: BaseExporter.event
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

View File

@@ -35,7 +35,8 @@ from rest_framework.reverse import reverse
from pretix.api.serializers.exporters import (
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.signals import (
register_data_exporters, register_multievent_data_exporters,
@@ -155,7 +156,19 @@ class OrganizerExportersViewSet(ExportersMixin, viewsets.ViewSet):
organizer=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)
exporters.append(ex)
return exporters

View File

@@ -51,7 +51,7 @@ from pretix.helpers.safe_openpyxl import ( # NOQA: backwards compatibility for
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:
@@ -80,7 +80,7 @@ class BaseExporter:
def verbose_name(self) -> str:
"""
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
@@ -137,6 +137,16 @@ class BaseExporter:
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):
ProgressSetTotal = namedtuple('ProgressSetTotal', 'total')

View File

@@ -60,7 +60,9 @@ from pretix.base.settings import PERSON_NAME_SCHEMES
from ...control.forms.filter import get_all_payment_providers
from ...helpers import GroupConcat
from ...helpers.iter import chunked_iterable
from ..exporter import ListExporter, MultiSheetListExporter
from ..exporter import (
ListExporter, MultiSheetListExporter, OrganizerLevelExportMixin,
)
from ..signals import (
register_data_exporters, register_multievent_data_exporters,
)
@@ -884,9 +886,10 @@ class QuotaListExporter(ListExporter):
return '{}_quotas'.format(self.event.slug)
class GiftcardTransactionListExporter(ListExporter):
class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
identifier = 'giftcardtransactionlist'
verbose_name = gettext_lazy('Gift card transactions')
organizer_required_permission = 'can_manage_gift_cards'
@property
def additional_form_fields(self):
@@ -998,9 +1001,10 @@ class GiftcardRedemptionListExporter(ListExporter):
return '{}_giftcardredemptions'.format(self.event.slug)
class GiftcardListExporter(ListExporter):
class GiftcardListExporter(OrganizerLevelExportMixin, ListExporter):
identifier = 'giftcardlist'
verbose_name = gettext_lazy('Gift cards')
organizer_required_permission = 'can_manage_gift_cards'
@property
def additional_form_fields(self):

View File

@@ -26,6 +26,7 @@ from django.core.files.base import ContentFile
from django.utils.timezone import override
from django.utils.translation import gettext
from pretix.base.exporter import OrganizerLevelExportMixin
from pretix.base.i18n import LazyLocaleException, language
from pretix.base.models import (
CachedFile, Device, Event, Organizer, TeamAPIToken, User, cachedfile_name,
@@ -119,6 +120,17 @@ def multiexport(self, organizer: Organizer, user: User, device: int, token: int,
continue
ex = response(events, organizer, set_progress)
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)
if d is None:
raise ExportError(

View File

@@ -65,6 +65,7 @@ from pretix.api.models import WebHook
from pretix.api.webhooks import manually_retry_all_calls
from pretix.base.auth import get_auth_backends
from pretix.base.channels import get_all_sales_channels
from pretix.base.exporter import OrganizerLevelExportMixin
from pretix.base.i18n import language
from pretix.base.models import (
CachedFile, Customer, Device, Gate, GiftCard, Invoice, LogEntry,
@@ -1508,7 +1509,19 @@ class ExportMixin:
)
responses = register_multievent_data_exporters.send(self.request.organizer)
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:
continue
@@ -1526,18 +1539,19 @@ class ExportMixin:
initial=initial
)
ex.form.fields = ex.export_form_fields
ex.form.fields.update([
('events',
forms.ModelMultipleChoiceField(
queryset=events,
initial=events,
widget=forms.CheckboxSelectMultiple(
attrs={'class': 'scrolling-multiple-choice'}
),
label=_('Events'),
required=True
)),
])
if not isinstance(ex, OrganizerLevelExportMixin):
ex.form.fields.update([
('events',
forms.ModelMultipleChoiceField(
queryset=events,
initial=events,
widget=forms.CheckboxSelectMultiple(
attrs={'class': 'scrolling-multiple-choice'}
),
label=_('Events'),
required=True
)),
])
exporters.append(ex)
return exporters

View File

@@ -191,3 +191,19 @@ def test_gone_without_celery(token_client, organizer, team, event):
cf = CachedFile.objects.create()
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
@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