mirror of
https://github.com/pretix/pretix.git
synced 2026-08-25 13:12:00 +00:00
Allow organizer-level exports with separate permission and no event selection
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user