mirror of
https://github.com/pretix/pretix.git
synced 2026-08-09 10:37:50 +00:00
Clean up logic around exporters
This commit is contained in:
@@ -27,7 +27,9 @@ from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.api.serializers.forms import form_field_to_serializer_field
|
||||
from pretix.base.exporter import OrganizerLevelExportMixin
|
||||
from pretix.base.models import ScheduledEventExport, ScheduledOrganizerExport
|
||||
from pretix.base.models import (
|
||||
Event, ScheduledEventExport, ScheduledOrganizerExport,
|
||||
)
|
||||
from pretix.base.timeframes import SerializerDateFrameField
|
||||
|
||||
|
||||
@@ -54,19 +56,28 @@ class ExporterSerializer(serializers.Serializer):
|
||||
|
||||
class JobRunSerializer(serializers.Serializer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
ex = kwargs.pop('exporter')
|
||||
ex = self.ex = kwargs.pop('exporter')
|
||||
super().__init__(*args, **kwargs)
|
||||
if ex.is_multievent and not isinstance(ex, OrganizerLevelExportMixin):
|
||||
self.fields["all_events"] = serializers.BooleanField(
|
||||
required=False,
|
||||
)
|
||||
self.fields["events"] = serializers.SlugRelatedField(
|
||||
queryset=ex.events,
|
||||
required=False,
|
||||
allow_empty=False,
|
||||
allow_empty=True,
|
||||
slug_field='slug',
|
||||
many=True
|
||||
)
|
||||
for k, v in ex.export_form_fields.items():
|
||||
self.fields[k] = form_field_to_serializer_field(v)
|
||||
|
||||
def to_representation(self, instance):
|
||||
# Translate between events as a list of slugs (API) and list of ints (database)
|
||||
if self.ex.is_multievent and not isinstance(self.ex, OrganizerLevelExportMixin) and "events" in instance and isinstance(instance["events"], list):
|
||||
instance["events"] = [e.slug for e in self.ex.events.filter(pk__in=instance["events"]).only("slug")]
|
||||
return instance
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if isinstance(data, QueryDict):
|
||||
data = data.copy()
|
||||
@@ -94,6 +105,14 @@ class JobRunSerializer(serializers.Serializer):
|
||||
data[fk] = f'{d_from.isoformat() if d_from else ""}/{d_to.isoformat() if d_to else ""}'
|
||||
|
||||
data = super().to_internal_value(data)
|
||||
|
||||
# Translate between events as a list of slugs (API) and list of ints (database)
|
||||
if self.ex.is_multievent and not isinstance(self.ex, OrganizerLevelExportMixin) and "events" in data and isinstance(data["events"], list):
|
||||
if data["events"] and isinstance(data["events"][0], Event):
|
||||
data["events"] = [e.pk for e in data["events"]]
|
||||
elif data["events"] and isinstance(data["events"][0], str):
|
||||
data["events"] = [e.pk for e in self.ex.events.filter(slug__in=data["events"]).only("pk")]
|
||||
|
||||
return data
|
||||
|
||||
def is_valid(self, raise_exception=False):
|
||||
@@ -130,13 +149,20 @@ class ScheduledExportSerializer(serializers.ModelSerializer):
|
||||
exporter = self.context['exporters'].get(identifier)
|
||||
if exporter:
|
||||
try:
|
||||
JobRunSerializer(exporter=exporter).to_internal_value(attrs["export_form_data"])
|
||||
attrs["export_form_data"] = JobRunSerializer(exporter=exporter).to_internal_value(attrs["export_form_data"])
|
||||
except ValidationError as e:
|
||||
raise ValidationError({"export_form_data": e.detail})
|
||||
else:
|
||||
raise ValidationError({"export_identifier": ["Unknown exporter."]})
|
||||
return attrs
|
||||
|
||||
def to_representation(self, instance):
|
||||
repr = super().to_representation(instance)
|
||||
exporter = self.context['exporters'].get(instance.export_identifier)
|
||||
if exporter:
|
||||
repr["export_form_data"] = JobRunSerializer(exporter=exporter).to_representation(repr["export_form_data"])
|
||||
return repr
|
||||
|
||||
def validate_mail_additional_recipients(self, value):
|
||||
d = value.replace(' ', '')
|
||||
if len(d.split(',')) > 25:
|
||||
|
||||
@@ -38,10 +38,12 @@ from pretix.api.serializers.exporters import (
|
||||
ExporterSerializer, JobRunSerializer, ScheduledEventExportSerializer,
|
||||
ScheduledOrganizerExportSerializer,
|
||||
)
|
||||
from pretix.base.exporter import OrganizerLevelExportMixin
|
||||
from pretix.base.models import (
|
||||
CachedFile, Device, ScheduledEventExport, ScheduledOrganizerExport,
|
||||
TeamAPIToken,
|
||||
)
|
||||
from pretix.base.models.organizer import TeamQuerySet
|
||||
from pretix.base.services.export import (
|
||||
export, init_event_exporters, init_organizer_exporters, multiexport,
|
||||
)
|
||||
@@ -203,7 +205,7 @@ class ScheduledExportersViewSet(viewsets.ModelViewSet):
|
||||
class ScheduledEventExportViewSet(ScheduledExportersViewSet):
|
||||
serializer_class = ScheduledEventExportSerializer
|
||||
queryset = ScheduledEventExport.objects.none()
|
||||
permission = 'event.orders:read'
|
||||
permission = None
|
||||
|
||||
def get_queryset(self):
|
||||
perm_holder = self.request.auth if isinstance(self.request.auth, (TeamAPIToken, Device)) else self.request.user
|
||||
@@ -249,6 +251,18 @@ class ScheduledEventExportViewSet(ScheduledExportersViewSet):
|
||||
return {e.identifier: e for e in exporters}
|
||||
|
||||
def perform_update(self, serializer):
|
||||
if not self.request.user.is_authenticated or self.request.user != serializer.instance.owner:
|
||||
# This is to prevent a possible privilege escalation where user A creates a scheduled export and
|
||||
# user B has settings permission (= they can see the export configuration), but not enough permission
|
||||
# to run the export themselves. Without this check, user B could modify the export and add themselves
|
||||
# as a recipient. Thereby, user B would gain access to data they can't have.
|
||||
exporter = self.exporters.get(serializer.instance.export_identifier)
|
||||
if not exporter:
|
||||
raise PermissionDenied("No access to exporter.")
|
||||
perm_holder = self.request.auth if isinstance(self.request.auth, (TeamAPIToken, Device)) else self.request.user
|
||||
if not perm_holder.has_event_permission(self.request.organizer, self.request.event, exporter.get_required_event_permission()):
|
||||
raise PermissionDenied("No permission to edit exports you could not run.")
|
||||
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.compute_next_run()
|
||||
serializer.instance.error_counter = 0
|
||||
@@ -319,6 +333,43 @@ class ScheduledOrganizerExportViewSet(ScheduledExportersViewSet):
|
||||
return {e.identifier: e for e in exporters}
|
||||
|
||||
def perform_update(self, serializer):
|
||||
if not self.request.user.is_authenticated or self.request.user != serializer.instance.owner:
|
||||
# This is to prevent a possible privilege escalation where user A creates a scheduled export and
|
||||
# user B has settings permission (= they can see the export configuration), but not enough permission
|
||||
# to run the export themselves. Without this check, user B could modify the export and add themselves
|
||||
# as a recipient. Thereby, user B would gain access to data they can't have.
|
||||
exporter = self.exporters.get(serializer.instance.export_identifier)
|
||||
if not exporter:
|
||||
raise PermissionDenied("No access to exporter.")
|
||||
perm_holder = (self.request.auth if isinstance(self.request.auth, (Device, TeamAPIToken))
|
||||
else self.request.user)
|
||||
if isinstance(exporter, OrganizerLevelExportMixin):
|
||||
if not perm_holder.has_organizer_permission(
|
||||
self.request.organizer, exporter.get_required_organizer_permission(), request=self.request,
|
||||
):
|
||||
raise PermissionDenied("No permission to edit exports you could not run.")
|
||||
else:
|
||||
if serializer.instance.export_form_data.get("all_events", False):
|
||||
if isinstance(self.request.auth, Device):
|
||||
if not self.request.auth.all_events:
|
||||
raise PermissionDenied("No permission to edit exports you could not run.")
|
||||
elif isinstance(self.request.auth, TeamAPIToken):
|
||||
if not self.request.auth.team.all_events:
|
||||
raise PermissionDenied("No permission to edit exports you could not run.")
|
||||
elif self.request.user.is_authenticated:
|
||||
if not self.request.user.teams.filter(
|
||||
TeamQuerySet.event_permission_q(exporter.get_required_event_permission()),
|
||||
all_events=True,
|
||||
).exists():
|
||||
raise PermissionDenied("No permission to edit exports you could not run.")
|
||||
else:
|
||||
events_selected = serializer.instance.export_form_data.get("events", [])
|
||||
events_permission = set(perm_holder.get_events_with_permission(
|
||||
exporter.get_required_event_permission(), request=self.request
|
||||
).values_list("pk", flat=True))
|
||||
if not all(e in events_permission for e in events_selected):
|
||||
raise PermissionDenied("No permission to edit exports you could not run.")
|
||||
|
||||
serializer.save(organizer=self.request.organizer)
|
||||
serializer.instance.compute_next_run()
|
||||
serializer.instance.error_counter = 0
|
||||
|
||||
@@ -271,7 +271,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
|
||||
qs = self._date_filter(qs, form_data, rel='')
|
||||
|
||||
if form_data['paid_only']:
|
||||
if form_data.get('paid_only'):
|
||||
qs = qs.filter(status=Order.STATUS_PAID)
|
||||
return qs
|
||||
|
||||
@@ -457,7 +457,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
).annotate(
|
||||
payment_providers=Subquery(p_providers, output_field=CharField()),
|
||||
).select_related('order', 'order__invoice_address', 'order__customer', 'tax_rule')
|
||||
if form_data['paid_only']:
|
||||
if form_data.get('paid_only'):
|
||||
qs = qs.filter(order__status=Order.STATUS_PAID, canceled=False)
|
||||
|
||||
if form_data.get('items'):
|
||||
@@ -560,7 +560,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
qs = OrderPosition.all.filter(
|
||||
order__event__in=self.events,
|
||||
)
|
||||
if form_data['paid_only']:
|
||||
if form_data.get('paid_only'):
|
||||
qs = qs.filter(order__status=Order.STATUS_PAID, canceled=False)
|
||||
|
||||
if form_data.get('items'):
|
||||
|
||||
@@ -134,7 +134,7 @@ def multiexport(self, organizer: Organizer, user: User, device: int, token: int,
|
||||
|
||||
event_qs = organizer.events.all()
|
||||
if form_data.get('events') is not None and not form_data.get('all_events'):
|
||||
if isinstance(form_data['events'][0], str):
|
||||
if form_data['events'] and isinstance(form_data['events'][0], str): # legacy API-created schedules
|
||||
event_qs = event_qs.filter(slug__in=form_data.get('events'))
|
||||
else:
|
||||
event_qs = event_qs.filter(pk__in=form_data.get('events'))
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
{% if s.export_verbose_name == "?" %}
|
||||
<strong class="text-danger">
|
||||
<span class="fa fa-warning fa-fw"></span>
|
||||
{% trans "Exporter not found" %}
|
||||
{% trans "Exporter not found or no permission" %}
|
||||
</strong>
|
||||
{% elif s.error_counter >= 5 %}
|
||||
<strong class="text-danger">
|
||||
|
||||
@@ -39,16 +39,18 @@
|
||||
</fieldset>
|
||||
{% if schedule_form %}
|
||||
{% include "pretixcontrol/orders/fragment_export_schedule_form.html" %}
|
||||
<div class="form-group submit-group">
|
||||
<button formaction="{{ request.get_full_path }}" name="schedule" value="save" type="submit"
|
||||
class="btn btn-primary btn-save" data-no-asynctask>
|
||||
{% if scheduled_copy_from %}
|
||||
{% trans "Save copy" %}
|
||||
{% else %}
|
||||
{% trans "Save" %}
|
||||
{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
{% if not no_save %}
|
||||
<div class="form-group submit-group">
|
||||
<button formaction="{{ request.get_full_path }}" name="schedule" value="save" type="submit"
|
||||
class="btn btn-primary btn-save" data-no-asynctask>
|
||||
{% if scheduled_copy_from %}
|
||||
{% trans "Save copy" %}
|
||||
{% else %}
|
||||
{% trans "Save" %}
|
||||
{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
{% if s.export_verbose_name == "?" %}
|
||||
<strong class="text-danger">
|
||||
<span class="fa fa-warning fa-fw"></span>
|
||||
{% trans "Exporter not found" %}
|
||||
{% trans "Exporter not found or no permission" %}
|
||||
</strong>
|
||||
{% elif s.error_counter >= 5 %}
|
||||
<strong class="text-danger">
|
||||
|
||||
@@ -40,16 +40,18 @@
|
||||
</fieldset>
|
||||
{% if schedule_form %}
|
||||
{% include "pretixcontrol/orders/fragment_export_schedule_form.html" %}
|
||||
<div class="form-group submit-group">
|
||||
<button formaction="{{ request.get_full_path }}" name="schedule" value="save" type="submit"
|
||||
class="btn btn-primary btn-save" data-no-asynctask>
|
||||
{% if scheduled_copy_from %}
|
||||
{% trans "Save copy" %}
|
||||
{% else %}
|
||||
{% trans "Save" %}
|
||||
{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
{% if not no_save %}
|
||||
<div class="form-group submit-group">
|
||||
<button formaction="{{ request.get_full_path }}" name="schedule" value="save" type="submit"
|
||||
class="btn btn-primary btn-save" data-no-asynctask>
|
||||
{% if scheduled_copy_from %}
|
||||
{% trans "Save copy" %}
|
||||
{% else %}
|
||||
{% trans "Save" %}
|
||||
{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
|
||||
@@ -2791,7 +2791,16 @@ class ExportView(EventPermissionRequiredMixin, ExportMixin, ListView):
|
||||
@transaction.atomic()
|
||||
def post(self, request, *args, **kwargs):
|
||||
if request.POST.get("schedule") == "save":
|
||||
if not self.has_permission():
|
||||
if self.scheduled and self.scheduled.pk and not self.has_permission_to_edit_scheduled():
|
||||
messages.error(
|
||||
self.request,
|
||||
_(
|
||||
"Your user account does not have sufficient permission to run this report, therefore "
|
||||
"you cannot change it."
|
||||
)
|
||||
)
|
||||
return super().get(request, *args, **kwargs)
|
||||
elif (not self.scheduled or not self.scheduled.pk) and not self.has_permission_to_create_scheduled():
|
||||
messages.error(
|
||||
self.request,
|
||||
_(
|
||||
@@ -2879,8 +2888,32 @@ class ExportView(EventPermissionRequiredMixin, ExportMixin, ListView):
|
||||
def get_queryset(self):
|
||||
return self.get_scheduled_queryset()
|
||||
|
||||
def has_permission(self):
|
||||
return self.request.user.has_event_permission(self.request.organizer, self.request.event, "event.orders:read")
|
||||
def has_permission_to_edit_scheduled(self):
|
||||
# Exports can be edited by
|
||||
# - their owner
|
||||
# - any staff session user
|
||||
# - any user with permission for organizer settings *and* the permissions required to run the rport
|
||||
# This is to prevent a possible privilege escalation where user A creates a scheduled export and
|
||||
# user B has settings permission (= they can see the export configuration), but not enough permission
|
||||
# to run the export themselves. Without this check, user B could modify the export and add themselves
|
||||
# as a recipient. Thereby, user B would gain access to data they can't have.
|
||||
if not self.exporter:
|
||||
return False
|
||||
if self.scheduled.owner == self.request.user:
|
||||
return True
|
||||
if self.request.user.has_active_staff_session(self.request.session.session_key):
|
||||
return True
|
||||
if not self.exporter.available_for_user(self.request.user):
|
||||
return False
|
||||
if self.request.user.has_event_permission(self.request.organizer, self.request.event,
|
||||
"event.settings.general:write", request=self.request):
|
||||
return self.request.user.has_event_permission(self.request.organizer, self.request.event,
|
||||
self.exporter.get_required_event_permission())
|
||||
|
||||
def has_permission_to_create_scheduled(self):
|
||||
# Exports can only be created if the user has the correct permissions. We *ignore* staff sessions, because
|
||||
# the export is not *run* during a staff session and then would fail at the scheduled time.
|
||||
return self.request.user.has_event_permission(self.request.organizer, self.request.event, self.exporter.get_required_event_permission())
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
@@ -2889,6 +2922,15 @@ class ExportView(EventPermissionRequiredMixin, ExportMixin, ListView):
|
||||
ctx['schedule_form'] = self.schedule_form
|
||||
ctx['rrule_form'] = self.rrule_form
|
||||
ctx['scheduled_copy_from'] = self.scheduled_copy_from
|
||||
|
||||
if self.scheduled and self.scheduled.pk and not self.has_permission_to_edit_scheduled() and self.exporter:
|
||||
ctx['no_save'] = True
|
||||
for f in self.exporter.form.fields.values():
|
||||
f.disabled = True
|
||||
for f in self.rrule_form.fields.values():
|
||||
f.disabled = True
|
||||
for f in self.schedule_form.fields.values():
|
||||
f.disabled = True
|
||||
elif not self.exporter:
|
||||
for s in ctx['scheduled']:
|
||||
try:
|
||||
|
||||
@@ -2122,7 +2122,16 @@ class ExportView(OrganizerPermissionRequiredMixin, ExportMixin, ListView):
|
||||
@transaction.atomic()
|
||||
def post(self, request, *args, **kwargs):
|
||||
if request.POST.get("schedule") == "save":
|
||||
if not self.has_permission():
|
||||
if self.scheduled and self.scheduled.pk and not self.has_permission_to_edit_scheduled():
|
||||
messages.error(
|
||||
self.request,
|
||||
_(
|
||||
"Your user account does not have sufficient permission to run this report, therefore "
|
||||
"you cannot change it."
|
||||
)
|
||||
)
|
||||
return super().get(request, *args, **kwargs)
|
||||
elif (not self.scheduled or not self.scheduled.pk) and not self.has_permission_to_create_scheduled():
|
||||
messages.error(
|
||||
self.request,
|
||||
_(
|
||||
@@ -2213,8 +2222,48 @@ class ExportView(OrganizerPermissionRequiredMixin, ExportMixin, ListView):
|
||||
def get_queryset(self):
|
||||
return self.get_scheduled_queryset()
|
||||
|
||||
def has_permission(self):
|
||||
# Check if permission exists even without staff session
|
||||
def has_permission_to_edit_scheduled(self):
|
||||
# Exports can be edited by
|
||||
# - their owner
|
||||
# - any staff session user
|
||||
# - any user with permission for organizer settings *and* the permissions required to run the report
|
||||
# This is to prevent a possible privilege escalation where user A creates a scheduled export and
|
||||
# user B has settings permission (= they can see the export configuration), but not enough permission
|
||||
# to run the export themselves. Without this check, user B could modify the export and add themselves
|
||||
# as a recipient. Thereby, user B would gain access to data they can't have.
|
||||
if not self.exporter:
|
||||
# Triggered in scenario 5 in test_organizer_edit_restrictions
|
||||
return False
|
||||
if self.scheduled.owner == self.request.user:
|
||||
return True
|
||||
if self.request.user.has_active_staff_session(self.request.session.session_key):
|
||||
return True
|
||||
if not self.exporter.available_for_user(self.request.user):
|
||||
return False
|
||||
if self.request.user.has_organizer_permission(self.request.organizer, "organizer.settings.general:write", request=self.request):
|
||||
if isinstance(self.exporter, OrganizerLevelExportMixin):
|
||||
# Test scenario 5/6 in test_organizer_edit_restrictions
|
||||
return self.request.user.has_organizer_permission(
|
||||
self.request.organizer, self.exporter.get_required_organizer_permission(), request=self.request
|
||||
)
|
||||
else:
|
||||
if self.scheduled.export_form_data.get("all_events", False):
|
||||
# Test scenario 1/2 in test_organizer_edit_restrictions
|
||||
return self.request.user.teams.filter(
|
||||
TeamQuerySet.event_permission_q(self.exporter.get_required_event_permission()),
|
||||
all_events=True,
|
||||
).exists()
|
||||
else:
|
||||
# Test scenario 3/4 in test_organizer_edit_restrictions
|
||||
events_selected = self.scheduled.export_form_data.get("events", [])
|
||||
events_permission = set(self.request.user.get_events_with_permission(
|
||||
self.exporter.get_required_event_permission(), request=self.request,
|
||||
).values_list("pk", flat=True))
|
||||
return all(e in events_permission for e in events_selected)
|
||||
|
||||
def has_permission_to_create_scheduled(self):
|
||||
# Exports can only be created if the user has the correct permissions. We *ignore* staff sessions, because
|
||||
# the export is not *run* during a staff session and then would fail at the scheduled time.
|
||||
if self.exporter:
|
||||
if isinstance(self.exporter, OrganizerLevelExportMixin):
|
||||
if not self.request.user.has_organizer_permission(self.request.organizer, self.exporter.get_required_organizer_permission()):
|
||||
@@ -2233,6 +2282,17 @@ class ExportView(OrganizerPermissionRequiredMixin, ExportMixin, ListView):
|
||||
ctx['schedule_form'] = self.schedule_form
|
||||
ctx['rrule_form'] = self.rrule_form
|
||||
ctx['scheduled_copy_from'] = self.scheduled_copy_from
|
||||
|
||||
if self.scheduled and self.scheduled.pk and not self.has_permission_to_edit_scheduled() and self.exporter:
|
||||
ctx['no_save'] = True
|
||||
for f in self.exporter.form.fields.values():
|
||||
f.disabled = True
|
||||
f.widget.attrs.pop("data-inverse-dependency", None)
|
||||
for f in self.rrule_form.fields.values():
|
||||
f.disabled = True
|
||||
for f in self.schedule_form.fields.values():
|
||||
f.disabled = True
|
||||
|
||||
elif not self.exporter:
|
||||
for s in ctx['scheduled']:
|
||||
try:
|
||||
|
||||
@@ -39,8 +39,11 @@ from datetime import time
|
||||
|
||||
import pytest
|
||||
from django.utils.timezone import now
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from pretix.base.models import CachedFile, User
|
||||
from pretix.base.models import (
|
||||
CachedFile, Event, ScheduledEventExport, ScheduledOrganizerExport, User,
|
||||
)
|
||||
|
||||
SAMPLE_EXPORTER_CONFIG = {
|
||||
"identifier": "orderlist",
|
||||
@@ -111,6 +114,10 @@ def test_org_list(token_client, organizer, event):
|
||||
"name": "events",
|
||||
"required": False
|
||||
})
|
||||
c['input_parameters'].insert(0, {
|
||||
"name": "all_events",
|
||||
"required": False
|
||||
})
|
||||
c['input_parameters'].remove({
|
||||
"name": "items",
|
||||
"required": False
|
||||
@@ -144,13 +151,6 @@ def test_org_validate_events(token_client, organizer, team, event):
|
||||
}, format='json')
|
||||
assert resp.status_code == 202
|
||||
|
||||
resp = token_client.post('/api/v1/organizers/{}/exporters/orderlist/run/'.format(organizer.slug), data={
|
||||
'_format': 'xlsx',
|
||||
'events': []
|
||||
}, format='json')
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {"events": ["This list may not be empty."]}
|
||||
|
||||
resp = token_client.post('/api/v1/organizers/{}/exporters/orderlist/run/'.format(organizer.slug), data={
|
||||
'_format': 'xlsx',
|
||||
'events': ["nonexisting"]
|
||||
@@ -824,3 +824,232 @@ def test_org_scheduled_export_validate_rrule(user_client, organizer, user):
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {"schedule_rrule": ["BYEASTER not supported"]}
|
||||
|
||||
|
||||
def _can_see_but_not_edit_org_export(client, scheduled):
|
||||
response = client.get(
|
||||
'/api/v1/organizers/{}/scheduled_exports/{}/'.format("dummy", scheduled.pk),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
response = client.patch(
|
||||
'/api/v1/organizers/{}/scheduled_exports/{}/'.format("dummy", scheduled.pk),
|
||||
data=response.data,
|
||||
format='json',
|
||||
)
|
||||
return response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_organizer_edit_restrictions(client, event, organizer, user, team):
|
||||
# This tests the prevention of a possible privilege escalation where user A creates a scheduled export and
|
||||
# user B has settings permission (= they can see the export configuration), but not enough permission
|
||||
# to run the export themselves. Without this check, user B could modify the export and add themselves
|
||||
# as a recipient. Thereby, user B would gain access to data they can't have.
|
||||
user1_client = APIClient()
|
||||
user1_client.force_authenticate(user=user)
|
||||
user2 = User.objects.create_user("dummy2@dummy.dummy", "dummy")
|
||||
user2_client = APIClient()
|
||||
user2_client.force_authenticate(user=user2)
|
||||
team1_client = APIClient()
|
||||
t = team.tokens.create(name='Foo')
|
||||
team1_client.credentials(HTTP_AUTHORIZATION='Token ' + t.token)
|
||||
|
||||
event1 = event
|
||||
event2 = Event.objects.create(
|
||||
organizer=organizer, name="Dummy", slug="dummy2",
|
||||
date_from=now(), plugins="pretix.plugins.banktransfer,pretix.plugins.stripe,tests.testdummy"
|
||||
)
|
||||
|
||||
team1 = team
|
||||
team1.all_organizer_permissions = False
|
||||
team1.all_event_permissions = False
|
||||
team1.all_events = False
|
||||
team1.limit_organizer_permissions = {"organizer.settings.general:write": True}
|
||||
team1.limit_event_permissions = {"event.orders:read": True, "event.settings.general:write": True}
|
||||
team1.save()
|
||||
team1.limit_events.add(event1)
|
||||
team1.members.add(user)
|
||||
|
||||
t = team.tokens.create(name='Foo')
|
||||
client.credentials(HTTP_AUTHORIZATION='Token ' + t.token)
|
||||
|
||||
team2 = organizer.teams.create(
|
||||
all_organizer_permissions=False, all_event_permissions=False, all_events=False,
|
||||
limit_event_permissions={"event.orders:read": True},
|
||||
limit_organizer_permissions={"organizer.giftcards:read": True}
|
||||
)
|
||||
team2.limit_events.add(event2)
|
||||
team2.members.add(user2)
|
||||
|
||||
# Scenario 1
|
||||
# User 2 created an export for all events. User 2 can edit it, because they own it.
|
||||
# User 1 can see it, because they have permission to see scheduled exports, but can't change it, because they
|
||||
# don't have access to all events.
|
||||
s1 = ScheduledOrganizerExport.objects.create(
|
||||
organizer=organizer,
|
||||
owner=user2,
|
||||
export_identifier="dummy_orders",
|
||||
export_form_data={"all_events": True, "events": []},
|
||||
mail_subject="Test",
|
||||
mail_template="Test",
|
||||
locale="en",
|
||||
schedule_rrule="DTSTART:20230118T000000\nRRULE:FREQ=DAILY;INTERVAL=1;WKST=MO",
|
||||
schedule_rrule_time=time(2, 30, 0)
|
||||
)
|
||||
user._teamcache = {}
|
||||
user2._teamcache = {}
|
||||
assert _can_see_but_not_edit_org_export(user2_client, s1)
|
||||
assert not _can_see_but_not_edit_org_export(user1_client, s1)
|
||||
assert not _can_see_but_not_edit_org_export(team1_client, s1)
|
||||
|
||||
# Scenario 2
|
||||
# User 2 created an export for all events. User 2 can edit it, because they own it.
|
||||
# User 1 can see it, because they have permission to see scheduled exports, and change it, because they
|
||||
# have access to all events.
|
||||
team1.all_events = True
|
||||
team1.save()
|
||||
user._teamcache = {}
|
||||
user2._teamcache = {}
|
||||
assert _can_see_but_not_edit_org_export(user2_client, s1)
|
||||
assert _can_see_but_not_edit_org_export(user1_client, s1)
|
||||
assert _can_see_but_not_edit_org_export(team1_client, s1)
|
||||
|
||||
# Scenario 3
|
||||
# User 2 created an export for a specific event. User 2 can edit it, because they own it.
|
||||
# User 1 can see it, because they have permission to see scheduled exports, but can't change it, because they
|
||||
# don't have access to that event.
|
||||
team1.all_events = False
|
||||
team1.save()
|
||||
s1.export_form_data = {"all_events": False, "events": [event2.pk]}
|
||||
s1.save()
|
||||
user._teamcache = {}
|
||||
user2._teamcache = {}
|
||||
assert _can_see_but_not_edit_org_export(user2_client, s1)
|
||||
assert not _can_see_but_not_edit_org_export(user1_client, s1)
|
||||
assert not _can_see_but_not_edit_org_export(team1_client, s1)
|
||||
|
||||
# Scenario 4
|
||||
# User 2 created an export for a specific event. User 2 can edit it, because they own it.
|
||||
# User 1 can see it, because they have permission to see scheduled exports, and change it, because they
|
||||
# have access to that event.
|
||||
team1.limit_events.add(event2)
|
||||
user._teamcache = {}
|
||||
user2._teamcache = {}
|
||||
assert _can_see_but_not_edit_org_export(user2_client, s1)
|
||||
assert _can_see_but_not_edit_org_export(user1_client, s1)
|
||||
assert _can_see_but_not_edit_org_export(team1_client, s1)
|
||||
|
||||
# Scenario 5
|
||||
# User 2 created an export that requires a special permission on organizer level
|
||||
# user 1 can see it, because they have permission to see scheduled exports, but can't change it, because they lack
|
||||
# that special permission
|
||||
s2 = ScheduledOrganizerExport.objects.create(
|
||||
organizer=organizer,
|
||||
owner=user2,
|
||||
export_identifier="giftcardlist",
|
||||
mail_subject="Test",
|
||||
mail_template="Test",
|
||||
locale="en",
|
||||
schedule_rrule="DTSTART:20230118T000000\nRRULE:FREQ=DAILY;INTERVAL=1;WKST=MO",
|
||||
schedule_rrule_time=time(2, 30, 0)
|
||||
)
|
||||
user._teamcache = {}
|
||||
user2._teamcache = {}
|
||||
assert _can_see_but_not_edit_org_export(user2_client, s2)
|
||||
assert not _can_see_but_not_edit_org_export(user1_client, s2)
|
||||
assert not _can_see_but_not_edit_org_export(team1_client, s2)
|
||||
|
||||
# Scenario 6
|
||||
# User 2 created an export that requires a special permission on organizer level
|
||||
# user 1 can see it, because they have permission to see scheduled exports, and change it, because they have
|
||||
# that special permission
|
||||
team1.limit_organizer_permissions["organizer.giftcards:read"] = True
|
||||
team1.save()
|
||||
user._teamcache = {}
|
||||
assert _can_see_but_not_edit_org_export(user2_client, s2)
|
||||
assert _can_see_but_not_edit_org_export(team1_client, s2)
|
||||
assert _can_see_but_not_edit_org_export(user1_client, s2)
|
||||
|
||||
|
||||
def _can_see_but_not_edit_event_export(client, scheduled):
|
||||
response = client.get(
|
||||
'/api/v1/organizers/{}/events/{}/scheduled_exports/{}/'.format("dummy", "dummy", scheduled.pk),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
response = client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/scheduled_exports/{}/'.format("dummy", "dummy", scheduled.pk),
|
||||
data=response.data,
|
||||
format='json',
|
||||
)
|
||||
return response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_event_edit_restrictions(client, event, organizer, user, team):
|
||||
# This tests the prevention of a possible privilege escalation where user A creates a scheduled export and
|
||||
# user B has settings permission (= they can see the export configuration), but not enough permission
|
||||
# to run the export themselves. Without this check, user B could modify the export and add themselves
|
||||
# as a recipient. Thereby, user B would gain access to data they can't have.
|
||||
user1_client = APIClient()
|
||||
user1_client.force_authenticate(user=user)
|
||||
user2 = User.objects.create_user("dummy2@dummy.dummy", "dummy")
|
||||
user2_client = APIClient()
|
||||
user2_client.force_authenticate(user=user2)
|
||||
team1_client = APIClient()
|
||||
t = team.tokens.create(name='Foo')
|
||||
team1_client.credentials(HTTP_AUTHORIZATION='Token ' + t.token)
|
||||
|
||||
event1 = event
|
||||
|
||||
team1 = team
|
||||
team1.all_organizer_permissions = False
|
||||
team1.all_event_permissions = False
|
||||
team1.all_events = False
|
||||
team1.limit_organizer_permissions = {"organizer.settings.general:write": True}
|
||||
team1.limit_event_permissions = {"event.orders:read": True, "event.settings.general:write": True}
|
||||
team1.save()
|
||||
team1.limit_events.add(event1)
|
||||
team1.members.add(user)
|
||||
|
||||
t = team.tokens.create(name='Foo')
|
||||
client.credentials(HTTP_AUTHORIZATION='Token ' + t.token)
|
||||
|
||||
team2 = organizer.teams.create(
|
||||
all_organizer_permissions=False, all_event_permissions=False, all_events=False,
|
||||
limit_event_permissions={"event.orders:read": True, "event.vouchers:read": True},
|
||||
limit_organizer_permissions={"organizer.giftcards:read": True}
|
||||
)
|
||||
team2.limit_events.add(event1)
|
||||
team2.members.add(user2)
|
||||
|
||||
# User 2 created an export that requires a special permission on organizer level
|
||||
# user 1 can see it, because they have permission to see scheduled exports, but can't change it, because they lack
|
||||
# that special permission
|
||||
s2 = ScheduledEventExport.objects.create(
|
||||
event=event,
|
||||
owner=user2,
|
||||
export_identifier="dummy_vouchers",
|
||||
mail_subject="Test",
|
||||
mail_template="Test",
|
||||
locale="en",
|
||||
schedule_rrule="DTSTART:20230118T000000\nRRULE:FREQ=DAILY;INTERVAL=1;WKST=MO",
|
||||
schedule_rrule_time=time(2, 30, 0)
|
||||
)
|
||||
user._teamcache = {}
|
||||
user2._teamcache = {}
|
||||
assert _can_see_but_not_edit_event_export(user2_client, s2)
|
||||
assert not _can_see_but_not_edit_event_export(user1_client, s2)
|
||||
assert not _can_see_but_not_edit_event_export(team1_client, s2)
|
||||
|
||||
# Scenario 6
|
||||
# User 2 created an export that requires a special permission on organizer level
|
||||
# user 1 can see it, because they have permission to see scheduled exports, and change it, because they have
|
||||
# that special permission
|
||||
team1.limit_event_permissions["event.vouchers:read"] = True
|
||||
team1.save()
|
||||
user._teamcache = {}
|
||||
assert _can_see_but_not_edit_event_export(user2_client, s2)
|
||||
assert _can_see_but_not_edit_event_export(team1_client, s2)
|
||||
assert _can_see_but_not_edit_event_export(user1_client, s2)
|
||||
|
||||
@@ -23,7 +23,9 @@ import datetime
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from bs4 import BeautifulSoup
|
||||
from django.utils.timezone import now
|
||||
from tests.base import extract_form_fields
|
||||
|
||||
from pretix.base.models import (
|
||||
Event, Item, Organizer, ScheduledEventExport, ScheduledOrganizerExport,
|
||||
@@ -378,3 +380,197 @@ def test_organizer_limited_permission(client, env):
|
||||
assert response.status_code == 200
|
||||
response = client.post(f"/control/organizer/dummy/export/{s2.pk}/run")
|
||||
assert response.status_code == 302
|
||||
|
||||
|
||||
def _can_see_but_not_edit_org_export(client, user, scheduled):
|
||||
client.login(email=user.email, password="dummy")
|
||||
|
||||
response = client.get("/control/organizer/dummy/export/")
|
||||
assert f"export/{scheduled.pk}/delete".encode() in response.content
|
||||
response = client.get(f"/control/organizer/dummy/export/?identifier={scheduled.export_identifier}&scheduled={scheduled.pk}")
|
||||
if response.status_code == 404:
|
||||
return False
|
||||
|
||||
assert response.status_code == 200
|
||||
doc = BeautifulSoup(response.content, "lxml")
|
||||
form_data = extract_form_fields(doc.select("form[data-asynctask]")[0])
|
||||
form_data["schedule"] = "save"
|
||||
|
||||
response = client.post(f"/control/organizer/dummy/export/?identifier={scheduled.export_identifier}&scheduled={scheduled.pk}",
|
||||
data=form_data, follow=True)
|
||||
assert response.status_code == 200
|
||||
|
||||
return b"alert-success" in response.content and b"does not have sufficient permission" not in response.content
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_organizer_edit_restrictions(client, env):
|
||||
# This tests the prevention of a possible privilege escalation where user A creates a scheduled export and
|
||||
# user B has settings permission (= they can see the export configuration), but not enough permission
|
||||
# to run the export themselves. Without this check, user B could modify the export and add themselves
|
||||
# as a recipient. Thereby, user B would gain access to data they can't have.
|
||||
user1 = env[1]
|
||||
user2 = User.objects.create_user("dummy2@dummy.dummy", "dummy")
|
||||
|
||||
event1 = env[0]
|
||||
event2 = Event.objects.create(
|
||||
organizer=env[0].organizer, name="Dummy", slug="dummy2",
|
||||
date_from=now(), plugins="pretix.plugins.banktransfer,pretix.plugins.stripe,tests.testdummy"
|
||||
)
|
||||
|
||||
team1 = env[2]
|
||||
team1.all_organizer_permissions = False
|
||||
team1.all_event_permissions = False
|
||||
team1.all_events = False
|
||||
team1.limit_organizer_permissions = {"organizer.settings.general:write": True}
|
||||
team1.limit_event_permissions = {"event.orders:read": True, "event.settings.general:write": True}
|
||||
team1.save()
|
||||
team1.limit_events.add(event1)
|
||||
|
||||
team2 = env[0].organizer.teams.create(
|
||||
all_organizer_permissions=False, all_event_permissions=False, all_events=False,
|
||||
limit_event_permissions={"event.orders:read": True},
|
||||
limit_organizer_permissions={"organizer.giftcards:read": True}
|
||||
)
|
||||
team2.limit_events.add(event2)
|
||||
team2.members.add(user2)
|
||||
|
||||
# Scenario 1
|
||||
# User 2 created an export for all events. User 2 can edit it, because they own it.
|
||||
# User 1 can see it, because they have permission to see scheduled exports, but can't change it, because they
|
||||
# don't have access to all events.
|
||||
s1 = ScheduledOrganizerExport.objects.create(
|
||||
organizer=env[0].organizer,
|
||||
owner=user2,
|
||||
export_identifier="dummy_orders",
|
||||
export_form_data={"all_events": True, "events": []},
|
||||
mail_subject="Test",
|
||||
mail_template="Test",
|
||||
schedule_rrule="DTSTART:20230118T000000\nRRULE:FREQ=DAILY;INTERVAL=1;WKST=MO",
|
||||
schedule_rrule_time=datetime.time(2, 30, 0)
|
||||
)
|
||||
assert _can_see_but_not_edit_org_export(client, user2, s1)
|
||||
assert not _can_see_but_not_edit_org_export(client, user1, s1)
|
||||
|
||||
# Scenario 2
|
||||
# User 2 created an export for all events. User 2 can edit it, because they own it.
|
||||
# User 1 can see it, because they have permission to see scheduled exports, and change it, because they
|
||||
# have access to all events.
|
||||
team1.all_events = True
|
||||
team1.save()
|
||||
assert _can_see_but_not_edit_org_export(client, user2, s1)
|
||||
assert _can_see_but_not_edit_org_export(client, user1, s1)
|
||||
|
||||
# Scenario 3
|
||||
# User 2 created an export for a specific event. User 2 can edit it, because they own it.
|
||||
# User 1 can see it, because they have permission to see scheduled exports, but can't change it, because they
|
||||
# don't have access to that event.
|
||||
team1.all_events = False
|
||||
team1.save()
|
||||
s1.export_form_data = {"all_events": False, "events": [event2.pk]}
|
||||
s1.save()
|
||||
assert _can_see_but_not_edit_org_export(client, user2, s1)
|
||||
assert not _can_see_but_not_edit_org_export(client, user1, s1)
|
||||
|
||||
# Scenario 4
|
||||
# User 2 created an export for a specific event. User 2 can edit it, because they own it.
|
||||
# User 1 can see it, because they have permission to see scheduled exports, and change it, because they
|
||||
# have access to that event.
|
||||
team1.limit_events.add(event2)
|
||||
assert _can_see_but_not_edit_org_export(client, user2, s1)
|
||||
assert _can_see_but_not_edit_org_export(client, user1, s1)
|
||||
|
||||
# Scenario 5
|
||||
# User 2 created an export that requires a special permission on organizer level
|
||||
# user 1 can see it, because they have permission to see scheduled exports, but can't change it, because they lack
|
||||
# that special permission
|
||||
s2 = ScheduledOrganizerExport.objects.create(
|
||||
organizer=env[0].organizer,
|
||||
owner=user2,
|
||||
export_identifier="giftcardlist",
|
||||
mail_subject="Test",
|
||||
mail_template="Test",
|
||||
schedule_rrule="DTSTART:20230118T000000\nRRULE:FREQ=DAILY;INTERVAL=1;WKST=MO",
|
||||
schedule_rrule_time=datetime.time(2, 30, 0)
|
||||
)
|
||||
assert _can_see_but_not_edit_org_export(client, user2, s2)
|
||||
assert not _can_see_but_not_edit_org_export(client, user1, s2)
|
||||
|
||||
# Scenario 6
|
||||
# User 2 created an export that requires a special permission on organizer level
|
||||
# user 1 can see it, because they have permission to see scheduled exports, and change it, because they have
|
||||
# that special permission
|
||||
team1.limit_organizer_permissions["organizer.giftcards:read"] = True
|
||||
team1.save()
|
||||
assert _can_see_but_not_edit_org_export(client, user2, s2)
|
||||
assert _can_see_but_not_edit_org_export(client, user1, s2)
|
||||
|
||||
|
||||
def _can_see_but_not_edit_event_export(client, user, scheduled):
|
||||
client.login(email=user.email, password="dummy")
|
||||
|
||||
response = client.get("/control/event/dummy/dummy/orders/export/")
|
||||
assert f"export/{scheduled.pk}/delete".encode() in response.content
|
||||
response = client.get(f"/control/event/dummy/dummy/orders/export/?identifier={scheduled.export_identifier}&scheduled={scheduled.pk}")
|
||||
if response.status_code == 404:
|
||||
return False
|
||||
|
||||
assert response.status_code == 200
|
||||
doc = BeautifulSoup(response.content, "lxml")
|
||||
form_data = extract_form_fields(doc.select("form[data-asynctask]")[0])
|
||||
form_data["schedule"] = "save"
|
||||
|
||||
response = client.post(f"/control/event/dummy/dummy/orders/export/?identifier={scheduled.export_identifier}&scheduled={scheduled.pk}",
|
||||
data=form_data, follow=True)
|
||||
assert response.status_code == 200
|
||||
|
||||
return b"alert-success" in response.content and b"does not have sufficient permission" not in response.content
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_event_edit_restrictions(client, env):
|
||||
# This tests the prevention of a possible privilege escalation where user A creates a scheduled export and
|
||||
# user B has settings permission (= they can see the export configuration), but not enough permission
|
||||
# to run the export themselves. Without this check, user B could modify the export and add themselves
|
||||
# as a recipient. Thereby, user B would gain access to data they can't have.
|
||||
user1 = env[1]
|
||||
user2 = User.objects.create_user("dummy2@dummy.dummy", "dummy")
|
||||
event1 = env[0]
|
||||
|
||||
team1 = env[2]
|
||||
team1.all_organizer_permissions = False
|
||||
team1.all_event_permissions = False
|
||||
team1.all_events = False
|
||||
team1.limit_organizer_permissions = {"organizer.settings.general:write": True}
|
||||
team1.limit_event_permissions = {"event.orders:read": True, "event.settings.general:write": True}
|
||||
team1.save()
|
||||
team1.limit_events.add(event1)
|
||||
|
||||
team2 = env[0].organizer.teams.create(
|
||||
all_organizer_permissions=False, all_event_permissions=False, all_events=False,
|
||||
limit_event_permissions={"event.orders:read": True, "event.vouchers:read": True},
|
||||
limit_organizer_permissions={"organizer.giftcards:read": True}
|
||||
)
|
||||
team2.limit_events.add(event1)
|
||||
team2.members.add(user2)
|
||||
|
||||
s2 = ScheduledEventExport.objects.create(
|
||||
event=event1,
|
||||
owner=user2,
|
||||
export_identifier="dummy_vouchers",
|
||||
mail_subject="Test",
|
||||
mail_template="Test",
|
||||
schedule_rrule="DTSTART:20230118T000000\nRRULE:FREQ=DAILY;INTERVAL=1;WKST=MO",
|
||||
schedule_rrule_time=datetime.time(2, 30, 0)
|
||||
)
|
||||
assert _can_see_but_not_edit_event_export(client, user2, s2)
|
||||
assert not _can_see_but_not_edit_event_export(client, user1, s2)
|
||||
|
||||
# Scenario 6
|
||||
# User 2 created an export that requires a special permission on organizer level
|
||||
# user 1 can see it, because they have permission to see scheduled exports, and change it, because they have
|
||||
# that special permission
|
||||
team1.limit_event_permissions["event.vouchers:read"] = True
|
||||
team1.save()
|
||||
assert _can_see_but_not_edit_event_export(client, user2, s2)
|
||||
assert _can_see_but_not_edit_event_export(client, user1, s2)
|
||||
|
||||
@@ -22,11 +22,13 @@
|
||||
from django.dispatch import receiver
|
||||
|
||||
from pretix.base.channels import SalesChannelType
|
||||
from pretix.base.exporter import BaseExporter
|
||||
from pretix.base.invoicing.transmission import (
|
||||
TransmissionProvider, transmission_providers,
|
||||
)
|
||||
from pretix.base.models import Invoice
|
||||
from pretix.base.signals import (
|
||||
register_data_exporters, register_multievent_data_exporters,
|
||||
register_payment_providers, register_sales_channel_types,
|
||||
register_ticket_outputs,
|
||||
)
|
||||
@@ -48,6 +50,40 @@ def register_payment_provider(sender, **kwargs):
|
||||
return [DummyPaymentProvider, DummyFullRefundablePaymentProvider, DummyPartialRefundablePaymentProvider]
|
||||
|
||||
|
||||
class DummyOrdersExporter(BaseExporter):
|
||||
verbose_name = "Dummy orders"
|
||||
identifier = "dummy_orders"
|
||||
|
||||
|
||||
class DummyVoucherExporter(BaseExporter):
|
||||
verbose_name = "Dummy orders"
|
||||
identifier = "dummy_vouchers"
|
||||
|
||||
@classmethod
|
||||
def get_required_event_permission(cls) -> str:
|
||||
return "event.vouchers:read"
|
||||
|
||||
|
||||
@receiver(register_data_exporters, dispatch_uid="dummy_exporter_o")
|
||||
def register_data_exporters_recv_o(sender, **kwargs):
|
||||
return DummyOrdersExporter
|
||||
|
||||
|
||||
@receiver(register_data_exporters, dispatch_uid="dummy_exporter_v")
|
||||
def register_data_exporters_recv_v(sender, **kwargs):
|
||||
return DummyVoucherExporter
|
||||
|
||||
|
||||
@receiver(register_multievent_data_exporters, dispatch_uid="dummy_exporter_multi_o")
|
||||
def register_multievent_data_exporters_recv_o(sender, **kwargs):
|
||||
return DummyOrdersExporter
|
||||
|
||||
|
||||
@receiver(register_multievent_data_exporters, dispatch_uid="dummy_exporter_multi_v")
|
||||
def register_multievent_data_exporters_recv(sender, **kwargs):
|
||||
return DummyVoucherExporter
|
||||
|
||||
|
||||
class FoobazSalesChannel(SalesChannelType):
|
||||
identifier = "baz"
|
||||
verbose_name = "Foobar"
|
||||
|
||||
Reference in New Issue
Block a user