Compare commits

...
Author SHA1 Message Date
Raphael Michel 3840f4fad2 Fix style issue 2026-06-22 09:22:31 +02:00
1f99890bc2 Apply suggestion from @pajowu
Co-authored-by: pajowu <engelhardt@pretix.eu>
2026-06-22 09:12:52 +02:00
Raphael Michel f050c2b45e Test order deletion: Improve bulk performance (Z#23237160) 2026-06-12 10:11:57 +02:00
3 changed files with 72 additions and 34 deletions
+50 -28
View File
@@ -354,38 +354,60 @@ class Order(LockModel, LoggedModel):
def _transaction_key_reset(self): def _transaction_key_reset(self):
self.__initial_status_paid_or_pending = self.status in (Order.STATUS_PENDING, Order.STATUS_PAID) and not self.require_approval self.__initial_status_paid_or_pending = self.status in (Order.STATUS_PENDING, Order.STATUS_PAID) and not self.require_approval
def gracefully_delete(self, user=None, auth=None): @classmethod
from . import GiftCard, GiftCardTransaction, Membership, Voucher def gracefully_delete_bulk(cls, event, orders, user=None, auth=None):
# Expects to be called in a transaction
if not self.testmode: from . import (
raise TypeError("Only test mode orders can be deleted.") GiftCard, GiftCardTransaction, LogEntry, Membership, Voucher,
self.log_action(
'pretix.event.order.deleted', user=user, auth=auth,
data={
'code': self.code,
}
) )
order_gracefully_delete.send(self.event, order=self) if not transaction.get_connection().in_atomic_block:
raise Exception('gracefully_delete_bulk should only be called in atomic transaction!')
if self.status != Order.STATUS_CANCELED: logs_create = []
for position in self.positions.all(): for o in orders:
if position.voucher: if not o.testmode:
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1)) raise TypeError("Only test mode orders can be deleted.")
order_gracefully_delete.send(event, order=o)
logs_create.append(o.log_action(
'pretix.event.order.deleted', user=user, auth=auth,
data={
'code': o.code,
},
save=False,
))
LogEntry.bulk_create_and_postprocess(logs_create)
GiftCardTransaction.objects.filter(payment__in=self.payments.all()).update(payment=None) voucher_ids = OrderPosition.objects.filter(
GiftCardTransaction.objects.filter(refund__in=self.refunds.all()).update(refund=None) order__in=orders,
GiftCardTransaction.objects.filter(order=self).update(order=None) voucher__isnull=False
GiftCard.objects.filter(issued_in__in=self.positions.all()).update(issued_in=None) ).exclude(order__status=Order.STATUS_CANCELED).values_list("voucher_id", flat=True)
Membership.objects.filter(granted_in__order=self, testmode=True).update(granted_in=None) voucher_usages = Counter(voucher_ids)
OrderPosition.all.filter(order=self, addon_to__isnull=False).delete() for v_id, usage_count in voucher_usages.items():
OrderPosition.all.filter(order=self).delete() Voucher.objects.filter(pk=v_id).update(redeemed=Greatest(0, F('redeemed') - usage_count))
OrderFee.all.filter(order=self).delete()
Transaction.objects.filter(order=self).delete() GiftCardTransaction.objects.filter(payment__order__in=orders).update(payment=None)
self.refunds.all().delete() GiftCardTransaction.objects.filter(refund__order__in=orders).update(refund=None)
self.payments.all().delete() GiftCardTransaction.objects.filter(order__in=orders).update(order=None)
self.event.cache.delete('complain_testmode_orders') GiftCard.objects.filter(issued_in__order__in=orders).update(issued_in=None)
self.delete() Membership.objects.filter(granted_in__order__in=orders, testmode=True).update(granted_in=None)
OrderPosition.all.filter(order__in=orders, addon_to__isnull=False).delete()
OrderPosition.all.filter(order__in=orders).delete()
OrderFee.all.filter(order__in=orders).delete()
Transaction.objects.filter(order__in=orders).delete()
OrderRefund.objects.filter(order__in=orders).delete()
OrderPayment.objects.filter(order__in=orders).delete()
if isinstance(orders, models.QuerySet):
orders.delete()
else:
Order.objects.filter(pk__in=[o.pk for o in orders]).delete()
event.cache.delete('complain_testmode_orders')
def gracefully_delete(self, user=None, auth=None):
if not self.testmode:
raise TypeError("Only test mode orders can be deleted.")
Order.gracefully_delete_bulk(self.event, Order.objects.filter(pk=self.pk), user, auth)
def email_confirm_secret(self): def email_confirm_secret(self):
return self.tagged_secret("email_confirm", 9) return self.tagged_secret("email_confirm", 9)
+5 -2
View File
@@ -1148,8 +1148,11 @@ class EventLive(EventPermissionRequiredMixin, TemplateView):
if request.POST.get("delete") == "yes": if request.POST.get("delete") == "yes":
try: try:
with transaction.atomic(): with transaction.atomic():
for order in request.event.orders.filter(testmode=True): Order.gracefully_delete_bulk(
order.gracefully_delete(user=self.request.user) request.event,
request.event.orders.filter(testmode=True),
user=self.request.user
)
except ProtectedError: except ProtectedError:
messages.error(self.request, _('An order could not be deleted as some constraints (e.g. data ' messages.error(self.request, _('An order could not be deleted as some constraints (e.g. data '
'created by plug-ins) do not allow it.')) 'created by plug-ins) do not allow it.'))
+17 -4
View File
@@ -139,6 +139,7 @@ from pretix.helpers import OF_SELF
from pretix.helpers.compat import CompatDeleteView from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.format import SafeFormatter, format_map from pretix.helpers.format import SafeFormatter, format_map
from pretix.helpers.hierarkey import clean_filename from pretix.helpers.hierarkey import clean_filename
from pretix.helpers.iter import chunked_iterable
from pretix.helpers.json import CustomJSONEncoder from pretix.helpers.json import CustomJSONEncoder
from pretix.helpers.safedownload import check_token from pretix.helpers.safedownload import check_token
from pretix.presale.signals import question_form_fields from pretix.presale.signals import question_form_fields
@@ -240,7 +241,7 @@ class BaseOrderBulkActionView(OrderSearchMixin, EventPermissionRequiredMixin, As
raise NotImplementedError() raise NotImplementedError()
def execute_bulk(self, queryset: QuerySet, form: forms.Form): def execute_bulk(self, queryset: QuerySet, form: forms.Form):
qs = self.allowed_for(self.allowed_for(self.get_queryset())) qs = self.allowed_for(self.get_queryset())
total = qs.count() total = qs.count()
orders_with_successful_action = 0 orders_with_successful_action = 0
for i, o in enumerate(qs): for i, o in enumerate(qs):
@@ -394,9 +395,21 @@ class OrderDeleteBulkActionView(BaseOrderBulkActionView):
testmode=True, testmode=True,
) )
def execute_single(self, instance, form: forms.Form): def execute_bulk(self, queryset: QuerySet, form: forms.Form):
instance.gracefully_delete(user=self.request.user) qs = self.allowed_for(self.get_queryset())
return True total = qs.count()
all_ids = list(qs.values_list("id", flat=True))
orders_with_successful_action = 0
for chunk in chunked_iterable(all_ids, 1000):
Order.gracefully_delete_bulk(
self.request.event,
qs.filter(id__in=chunk),
user=self.request.user,
)
orders_with_successful_action += len(chunk)
self.async_set_progress(orders_with_successful_action / total * 100)
return orders_with_successful_action, total
class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin, ListView): class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin, ListView):