diff --git a/src/pretix/api/views/checkin.py b/src/pretix/api/views/checkin.py
index 7bafddd3a3..1eb05b4940 100644
--- a/src/pretix/api/views/checkin.py
+++ b/src/pretix/api/views/checkin.py
@@ -139,6 +139,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
)
return qs
+ @transaction.atomic()
def perform_create(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
@@ -153,6 +154,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
ctx['event'] = self.request.event
return ctx
+ @transaction.atomic()
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
diff --git a/src/pretix/api/views/discount.py b/src/pretix/api/views/discount.py
index 53386d3d04..92bbd671e0 100644
--- a/src/pretix/api/views/discount.py
+++ b/src/pretix/api/views/discount.py
@@ -32,6 +32,7 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
+from django.db import transaction
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from rest_framework import viewsets
@@ -64,6 +65,7 @@ class DiscountViewSet(ConditionalListView, viewsets.ModelViewSet):
'limit_sales_channels',
)
+ @transaction.atomic()
def perform_create(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
@@ -78,6 +80,7 @@ class DiscountViewSet(ConditionalListView, viewsets.ModelViewSet):
ctx['event'] = self.request.event
return ctx
+ @transaction.atomic()
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
@@ -87,6 +90,7 @@ class DiscountViewSet(ConditionalListView, viewsets.ModelViewSet):
data=self.request.data
)
+ @transaction.atomic()
def perform_destroy(self, instance):
if not instance.allow_delete():
raise PermissionDenied('You cannot delete this discount because it already has '
diff --git a/src/pretix/api/views/event.py b/src/pretix/api/views/event.py
index 630988aacd..e92f99ed8b 100644
--- a/src/pretix/api/views/event.py
+++ b/src/pretix/api/views/event.py
@@ -257,6 +257,7 @@ class EventViewSet(viewsets.ModelViewSet):
data=self.request.data
)
+ @transaction.atomic()
def perform_create(self, serializer):
copy_from = None
if 'clone_from' in self.request.GET:
@@ -320,6 +321,7 @@ class EventViewSet(viewsets.ModelViewSet):
data=self.request.data
)
+ @transaction.atomic()
def perform_destroy(self, instance):
if not instance.allow_delete():
raise PermissionDenied('The event can not be deleted as it already contains orders. Please set \'live\''
@@ -355,6 +357,7 @@ class CloneEventViewSet(viewsets.ModelViewSet):
ctx['organizer'] = self.request.organizer
return ctx
+ @transaction.atomic()
def perform_create(self, serializer):
# Weird edge case: Requires settings permission on the event (to read) but also on the organizer (two write)
perm_holder = (self.request.auth if isinstance(self.request.auth, (Device, TeamAPIToken))
@@ -513,6 +516,7 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
resp['X-Page-Generated'] = date
return resp
+ @transaction.atomic()
def perform_update(self, serializer):
original_data = self.get_serializer(instance=serializer.instance).data
super().perform_update(serializer)
@@ -529,6 +533,7 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
data=self.request.data
)
+ @transaction.atomic()
def perform_create(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
@@ -538,6 +543,7 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
data=self.request.data
)
+ @transaction.atomic()
def perform_destroy(self, instance):
if not instance.allow_delete():
raise PermissionDenied('The sub-event can not be deleted as it has already been used in orders. Please set'
@@ -566,6 +572,7 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
def get_queryset(self):
return self.request.event.tax_rules.all()
+ @transaction.atomic()
def perform_update(self, serializer):
super().perform_update(serializer)
serializer.instance.log_action(
@@ -575,6 +582,7 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
data=self.request.data
)
+ @transaction.atomic()
def perform_create(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
@@ -584,6 +592,7 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
data=self.request.data
)
+ @transaction.atomic()
def perform_destroy(self, instance):
if not instance.allow_delete():
raise PermissionDenied('This tax rule can not be deleted as it is currently in use.')
@@ -757,6 +766,7 @@ class SeatViewSet(ConditionalListView, UpdateModelMixin, viewsets.ReadOnlyModelV
}
return ctx
+ @transaction.atomic()
def perform_update(self, serializer):
super().perform_update(serializer)
serializer.instance.event.log_action(
@@ -766,6 +776,7 @@ class SeatViewSet(ConditionalListView, UpdateModelMixin, viewsets.ReadOnlyModelV
data={"seats": [serializer.instance.pk]},
)
+ @transaction.atomic()
def bulk_change_blocked(self, blocked):
s = SeatBulkBlockInputSerializer(
data=self.request.data,
diff --git a/src/pretix/api/views/exporters.py b/src/pretix/api/views/exporters.py
index 26aca588f8..71931afcc1 100644
--- a/src/pretix/api/views/exporters.py
+++ b/src/pretix/api/views/exporters.py
@@ -23,6 +23,7 @@ from datetime import timedelta
from celery.result import AsyncResult
from django.conf import settings
+from django.db import transaction
from django.http import Http404
from django.shortcuts import get_object_or_404
from django.utils.functional import cached_property
@@ -227,6 +228,7 @@ class ScheduledEventExportViewSet(ScheduledExportersViewSet):
qs = self.request.event.scheduled_exports
return qs.select_related("owner")
+ @transaction.atomic()
def perform_create(self, serializer):
if not self.request.user.is_authenticated:
raise PermissionDenied('Creation of exports requires user-specific API access.')
@@ -257,6 +259,7 @@ class ScheduledEventExportViewSet(ScheduledExportersViewSet):
))
return {e.identifier: e for e in exporters}
+ @transaction.atomic()
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
@@ -282,6 +285,7 @@ class ScheduledEventExportViewSet(ScheduledExportersViewSet):
data=self.request.data
)
+ @transaction.atomic()
def perform_destroy(self, instance):
self.request.event.log_action(
'pretix.event.export.schedule.deleted',
@@ -309,6 +313,7 @@ class ScheduledOrganizerExportViewSet(ScheduledExportersViewSet):
qs = self.request.organizer.scheduled_exports
return qs.select_related("owner")
+ @transaction.atomic()
def perform_create(self, serializer):
if not self.request.user.is_authenticated:
raise PermissionDenied('Creation of exports requires user-specific API access.')
@@ -339,6 +344,7 @@ class ScheduledOrganizerExportViewSet(ScheduledExportersViewSet):
))
return {e.identifier: e for e in exporters}
+ @transaction.atomic()
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
@@ -389,6 +395,7 @@ class ScheduledOrganizerExportViewSet(ScheduledExportersViewSet):
data=self.request.data
)
+ @transaction.atomic()
def perform_destroy(self, instance):
self.request.organizer.log_action(
'pretix.organizer.export.schedule.deleted',
diff --git a/src/pretix/api/views/item.py b/src/pretix/api/views/item.py
index f2f0949392..8250600a0a 100644
--- a/src/pretix/api/views/item.py
+++ b/src/pretix/api/views/item.py
@@ -33,6 +33,7 @@
# License for the specific language governing permissions and limitations under the License.
import django_filters
+from django.db import transaction
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.utils.functional import cached_property
@@ -109,6 +110,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
'limit_sales_channels', 'variations__limit_sales_channels', 'program_times'
).all()
+ @transaction.atomic()
def perform_create(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
@@ -123,6 +125,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
ctx['event'] = self.request.event
return ctx
+ @transaction.atomic()
def perform_update(self, serializer):
original_data = self.get_serializer(instance=serializer.instance).data
@@ -139,6 +142,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
data=self.request.data
)
+ @transaction.atomic()
def perform_destroy(self, instance):
if not instance.allow_delete():
raise PermissionDenied('This item cannot be deleted because it has already been ordered '
@@ -183,6 +187,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
ctx['event'] = self.request.event
return ctx
+ @transaction.atomic()
def perform_create(self, serializer):
item = self.item
if not item.has_variations:
@@ -197,6 +202,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
{'value': serializer.instance.value})
)
+ @transaction.atomic()
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.item.log_action(
@@ -207,6 +213,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
{'value': serializer.instance.value})
)
+ @transaction.atomic()
def perform_destroy(self, instance):
if not instance.allow_delete():
raise PermissionDenied('This variation cannot be deleted because it has already been ordered '
@@ -249,6 +256,7 @@ class ItemBundleViewSet(viewsets.ModelViewSet):
ctx['item'] = self.item
return ctx
+ @transaction.atomic()
def perform_create(self, serializer):
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
serializer.save(base_item=item)
@@ -259,6 +267,7 @@ class ItemBundleViewSet(viewsets.ModelViewSet):
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
)
+ @transaction.atomic()
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.base_item.log_action(
@@ -268,6 +277,7 @@ class ItemBundleViewSet(viewsets.ModelViewSet):
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
)
+ @transaction.atomic()
def perform_destroy(self, instance):
super().perform_destroy(instance)
instance.base_item.log_action(
@@ -303,6 +313,7 @@ class ItemProgramTimeViewSet(viewsets.ModelViewSet):
ctx['item'] = self.item
return ctx
+ @transaction.atomic()
def perform_create(self, serializer):
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
serializer.save(item=item)
@@ -313,6 +324,7 @@ class ItemProgramTimeViewSet(viewsets.ModelViewSet):
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
)
+ @transaction.atomic()
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.item.log_action(
@@ -322,6 +334,7 @@ class ItemProgramTimeViewSet(viewsets.ModelViewSet):
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
)
+ @transaction.atomic()
def perform_destroy(self, instance):
super().perform_destroy(instance)
instance.item.log_action(
@@ -354,6 +367,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
ctx['item'] = self.item
return ctx
+ @transaction.atomic()
def perform_create(self, serializer):
item = self.item
category = get_object_or_404(ItemCategory, pk=self.request.data['addon_category'])
@@ -365,6 +379,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
)
+ @transaction.atomic()
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.base_item.log_action(
@@ -374,6 +389,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
)
+ @transaction.atomic()
def perform_destroy(self, instance):
super().perform_destroy(instance)
instance.base_item.log_action(
@@ -403,6 +419,7 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
def get_queryset(self):
return self.request.event.categories.all()
+ @transaction.atomic()
def perform_create(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
@@ -417,6 +434,7 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
ctx['event'] = self.request.event
return ctx
+ @transaction.atomic()
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
@@ -426,6 +444,7 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
data=self.request.data
)
+ @transaction.atomic()
def perform_destroy(self, instance):
for item in instance.items.all():
item.category = None
@@ -458,6 +477,7 @@ class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
def get_queryset(self):
return self.request.event.questions.prefetch_related('options').all()
+ @transaction.atomic()
def perform_create(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
@@ -472,6 +492,7 @@ class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
ctx['event'] = self.request.event
return ctx
+ @transaction.atomic()
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
@@ -481,6 +502,7 @@ class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
data=self.request.data
)
+ @transaction.atomic()
def perform_destroy(self, instance):
instance.log_action(
'pretix.event.question.deleted',
@@ -509,6 +531,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
ctx['question'] = get_object_or_404(Question, pk=self.kwargs['question'], event=self.request.event)
return ctx
+ @transaction.atomic()
def perform_create(self, serializer):
q = get_object_or_404(Question, pk=self.kwargs['question'], event=self.request.event)
serializer.save(question=q)
@@ -519,6 +542,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
)
+ @transaction.atomic()
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.question.log_action(
@@ -528,6 +552,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
)
+ @transaction.atomic()
def perform_destroy(self, instance):
instance.question.log_action(
'pretix.event.question.option.deleted',
@@ -586,6 +611,7 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
+ @transaction.atomic()
def perform_create(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
@@ -608,6 +634,7 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
ctx['request'] = self.request
return ctx
+ @transaction.atomic()
def perform_update(self, serializer):
original_data = self.get_serializer(instance=serializer.instance).data
@@ -663,6 +690,7 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
)
serializer.instance.rebuild_cache()
+ @transaction.atomic()
def perform_destroy(self, instance):
instance.log_action(
'pretix.event.quota.deleted',
diff --git a/src/pretix/api/views/organizer.py b/src/pretix/api/views/organizer.py
index 3551437b6f..db28044b30 100644
--- a/src/pretix/api/views/organizer.py
+++ b/src/pretix/api/views/organizer.py
@@ -394,6 +394,7 @@ class TeamViewSet(viewsets.ModelViewSet):
)
return inst
+ @transaction.atomic()
def perform_destroy(self, instance):
instance.log_action('pretix.team.deleted', user=self.request.user, auth=self.request.auth)
instance.delete()
@@ -693,6 +694,7 @@ class MembershipTypeViewSet(viewsets.ModelViewSet):
ctx['organizer'] = self.request.organizer
return ctx
+ @transaction.atomic()
def perform_destroy(self, instance):
if not instance.allow_delete():
raise PermissionDenied("Can only be deleted if unused.")
@@ -833,6 +835,7 @@ class SalesChannelViewSet(viewsets.ModelViewSet):
)
return inst
+ @transaction.atomic()
def perform_destroy(self, instance):
if not instance.allow_delete():
raise PermissionDenied("Can only be deleted if unused.")
diff --git a/src/pretix/api/views/waitinglist.py b/src/pretix/api/views/waitinglist.py
index 569c64b569..c2fd0b30ba 100644
--- a/src/pretix/api/views/waitinglist.py
+++ b/src/pretix/api/views/waitinglist.py
@@ -20,6 +20,7 @@
# .
#
import django_filters
+from django.db import transaction
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from rest_framework import viewsets
@@ -62,6 +63,7 @@ class WaitingListViewSet(viewsets.ModelViewSet):
ctx['event'] = self.request.event
return ctx
+ @transaction.atomic()
def perform_create(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
@@ -70,6 +72,7 @@ class WaitingListViewSet(viewsets.ModelViewSet):
auth=self.request.auth,
)
+ @transaction.atomic()
def perform_update(self, serializer):
if serializer.instance.voucher:
raise PermissionDenied('This entry can not be changed as it has already been assigned a voucher.')
@@ -80,6 +83,7 @@ class WaitingListViewSet(viewsets.ModelViewSet):
auth=self.request.auth,
)
+ @transaction.atomic()
def perform_destroy(self, instance):
if instance.voucher:
raise PermissionDenied('This entry can not be deleted as it has already been assigned a voucher.')
diff --git a/src/pretix/api/views/webhooks.py b/src/pretix/api/views/webhooks.py
index b7b883906c..d42c40c6bd 100644
--- a/src/pretix/api/views/webhooks.py
+++ b/src/pretix/api/views/webhooks.py
@@ -20,6 +20,7 @@
# .
#
import django_filters
+from django.db import transaction
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from rest_framework import viewsets
@@ -48,6 +49,7 @@ class WebHookViewSet(viewsets.ModelViewSet):
ctx['organizer'] = self.request.organizer
return ctx
+ @transaction.atomic()
def perform_create(self, serializer):
inst = serializer.save(organizer=self.request.organizer)
self.request.organizer.log_action(
@@ -57,6 +59,7 @@ class WebHookViewSet(viewsets.ModelViewSet):
data=merge_dicts(self.request.data, {'id': inst.pk})
)
+ @transaction.atomic()
def perform_update(self, serializer):
inst = serializer.save(organizer=self.request.organizer)
self.request.organizer.log_action(
@@ -67,6 +70,7 @@ class WebHookViewSet(viewsets.ModelViewSet):
)
return inst
+ @transaction.atomic()
def perform_destroy(self, instance):
self.request.organizer.log_action(
'pretix.webhook.changed',
diff --git a/src/pretix/plugins/sendmail/api.py b/src/pretix/plugins/sendmail/api.py
index 7d5e1548ae..2e751e5ef5 100644
--- a/src/pretix/plugins/sendmail/api.py
+++ b/src/pretix/plugins/sendmail/api.py
@@ -20,6 +20,7 @@
# .
#
from django.core.exceptions import ValidationError
+from django.db import transaction
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from rest_framework import viewsets
@@ -118,6 +119,7 @@ class RuleViewSet(viewsets.ModelViewSet):
def get_queryset(self):
return Rule.objects.filter(event=self.request.event)
+ @transaction.atomic()
def perform_create(self, serializer):
super().perform_create(serializer)
serializer.instance.log_action(
@@ -128,6 +130,7 @@ class RuleViewSet(viewsets.ModelViewSet):
)
+ @transaction.atomic()
def perform_update(self, serializer):
super().perform_update(serializer)
serializer.instance.log_action(
@@ -137,6 +140,7 @@ class RuleViewSet(viewsets.ModelViewSet):
data=self.request.data
)
+ @transaction.atomic()
def perform_destroy(self, instance):
instance.log_action(
'pretix.plugins.sendmail.rule.deleted',