Compare commits

..
Author SHA1 Message Date
Raphael Michel 3840f4fad2 Fix style issue 2026-06-22 09:22:31 +02:00
Raphael Michelandpajowu 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
8 changed files with 80 additions and 51 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)
+1 -1
View File
@@ -11,7 +11,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="icon" href="{% static "pretixbase/img/favicon.ico" %}"> <link rel="icon" href="{% static "pretixbase/img/favicon.ico" %}">
<script type="text/javascript" src="{% static "pretixbase/js/errors.js" %}"></script>
{% block custom_header %}{% endblock %} {% block custom_header %}{% endblock %}
{% if css_theme %} {% if css_theme %}
<link rel="stylesheet" type="text/css" href="{{ css_theme }}" /> <link rel="stylesheet" type="text/css" href="{{ css_theme }}" />
@@ -21,5 +20,6 @@
<div class="container"> <div class="container">
{% block content %}{% endblock %} {% block content %}{% endblock %}
</div> </div>
<script src="{% static "pretixbase/js/errors.js" %}"></script>
</body> </body>
</html> </html>
@@ -66,7 +66,6 @@
<script type="text/javascript" src="{% static "lightbox/js/lightbox.js" %}"></script> <script type="text/javascript" src="{% static "lightbox/js/lightbox.js" %}"></script>
<script type="text/javascript" src="{% static "are-you-sure/jquery.are-you-sure.js" %}"></script> <script type="text/javascript" src="{% static "are-you-sure/jquery.are-you-sure.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/addressform.js" %}"></script> <script type="text/javascript" src="{% static "pretixbase/js/addressform.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/errors.js" %}"></script>
{% endcompress %} {% endcompress %}
{{ html_head|safe }} {{ html_head|safe }}
+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):
@@ -22,5 +22,4 @@
<script type="text/javascript" src="{% static "pretixpresale/js/ui/iframe.js" %}"></script> <script type="text/javascript" src="{% static "pretixpresale/js/ui/iframe.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/addressform.js" %}"></script> <script type="text/javascript" src="{% static "pretixbase/js/addressform.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/deanonymize_email.js" %}"></script> <script type="text/javascript" src="{% static "pretixbase/js/deanonymize_email.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/errors.js" %}"></script>
{% endcompress %} {% endcompress %}
+2 -3
View File
@@ -174,16 +174,15 @@ function async_task_error(jqXHR, textStatus, errorThrown) {
var respdom = $(jqXHR.responseText); var respdom = $(jqXHR.responseText);
var c = respdom.filter('.container'); var c = respdom.filter('.container');
if (respdom.filter('form') && (respdom.filter('.has-error') || respdom.filter('.alert-danger'))) { if (respdom.filter('form') && (respdom.filter('.has-error') || respdom.filter('.alert-danger'))) {
// This is a failed form validation, let's just use it
if (respdom.filter('#page-wrapper') && $('#page-wrapper').length) { if (respdom.filter('#page-wrapper') && $('#page-wrapper').length) {
// This is a failed form validation, let's just use it
async_task_replace_page("#page-wrapper", respdom.find("#page-wrapper").html()); async_task_replace_page("#page-wrapper", respdom.find("#page-wrapper").html());
} else { } else {
async_task_replace_page("body", jqXHR.responseText.substring( async_task_replace_page("body", jqXHR.responseText.substring(
jqXHR.responseText.indexOf("<body"), jqXHR.responseText.indexOf("<body"),
jqXHR.responseText.indexOf("</body") jqXHR.responseText.indexOf("</body")
)); ));
document.dispatchEvent(new Event("pretix:async-task-error"))
} }
} else if (c.length > 0) { } else if (c.length > 0) {
+5 -11
View File
@@ -1,11 +1,5 @@
['DOMContentLoaded', 'pretix:async-task-error'].forEach(function (ev) { document.getElementById('goback').onclick =
document.addEventListener(ev, function () { function() {window.history.back()};
document.querySelectorAll('#goback, #reload').forEach(function (element) {
const regularLoad = ev === 'DOMContentLoaded' && element.id === 'goback'; document.getElementById('reload').onclick =
element.addEventListener('click', regularLoad function() {window.location.reload(true)};
? () => window.history.back()
: () => window.location.reload()
);
});
});
});