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
18 changed files with 100 additions and 130 deletions
+2 -1
View File
@@ -10,7 +10,8 @@ tests:
- cd src
- python manage.py check
- make all compress
- PRETIX_CONFIG_FILE=tests/ci_sqlite.cfg py.test -n 3 tests --ignore=tests/e2e --maxfail=100
- playwright install
- PRETIX_CONFIG_FILE=tests/ci_sqlite.cfg py.test -n 3 tests --maxfail=100
except:
- '/^v.*$/'
pypi:
+2 -4
View File
@@ -74,7 +74,6 @@ class LocaleMiddleware(MiddlewareMixin):
def process_request(self, request: HttpRequest):
language = get_language_from_request(request)
region = None
# Normally, this middleware runs *before* the event is set. However, on event frontend pages it
# might be run a second time by pretix.presale.EventMiddleware and in this case the event is already
# set and can be taken into account for the decision.
@@ -95,16 +94,15 @@ class LocaleMiddleware(MiddlewareMixin):
if '-' not in language and settings_holder.settings.region:
language += '-' + settings_holder.settings.region
if settings_holder.settings.region:
region = settings_holder.settings.region
set_region(settings_holder.settings.region)
else:
gs = global_settings_object(request)
if '-' not in language and gs.settings.region:
language += '-' + gs.settings.region
if gs.settings.region:
region = gs.settings.region
set_region(gs.settings.region)
translation.activate(language)
set_region(region)
request.LANGUAGE_CODE = get_language_without_region()
tzname = None
+50 -28
View File
@@ -354,38 +354,60 @@ class Order(LockModel, LoggedModel):
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
def gracefully_delete(self, user=None, auth=None):
from . import GiftCard, GiftCardTransaction, Membership, Voucher
if not self.testmode:
raise TypeError("Only test mode orders can be deleted.")
self.log_action(
'pretix.event.order.deleted', user=user, auth=auth,
data={
'code': self.code,
}
@classmethod
def gracefully_delete_bulk(cls, event, orders, user=None, auth=None):
# Expects to be called in a transaction
from . import (
GiftCard, GiftCardTransaction, LogEntry, Membership, Voucher,
)
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:
for position in self.positions.all():
if position.voucher:
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
logs_create = []
for o in orders:
if not o.testmode:
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)
GiftCardTransaction.objects.filter(refund__in=self.refunds.all()).update(refund=None)
GiftCardTransaction.objects.filter(order=self).update(order=None)
GiftCard.objects.filter(issued_in__in=self.positions.all()).update(issued_in=None)
Membership.objects.filter(granted_in__order=self, testmode=True).update(granted_in=None)
OrderPosition.all.filter(order=self, addon_to__isnull=False).delete()
OrderPosition.all.filter(order=self).delete()
OrderFee.all.filter(order=self).delete()
Transaction.objects.filter(order=self).delete()
self.refunds.all().delete()
self.payments.all().delete()
self.event.cache.delete('complain_testmode_orders')
self.delete()
voucher_ids = OrderPosition.objects.filter(
order__in=orders,
voucher__isnull=False
).exclude(order__status=Order.STATUS_CANCELED).values_list("voucher_id", flat=True)
voucher_usages = Counter(voucher_ids)
for v_id, usage_count in voucher_usages.items():
Voucher.objects.filter(pk=v_id).update(redeemed=Greatest(0, F('redeemed') - usage_count))
GiftCardTransaction.objects.filter(payment__order__in=orders).update(payment=None)
GiftCardTransaction.objects.filter(refund__order__in=orders).update(refund=None)
GiftCardTransaction.objects.filter(order__in=orders).update(order=None)
GiftCard.objects.filter(issued_in__order__in=orders).update(issued_in=None)
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):
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 charset="utf-8">
<link rel="icon" href="{% static "pretixbase/img/favicon.ico" %}">
<script type="text/javascript" src="{% static "pretixbase/js/errors.js" %}"></script>
{% block custom_header %}{% endblock %}
{% if css_theme %}
<link rel="stylesheet" type="text/css" href="{{ css_theme }}" />
@@ -21,5 +20,6 @@
<div class="container">
{% block content %}{% endblock %}
</div>
<script src="{% static "pretixbase/js/errors.js" %}"></script>
</body>
</html>
+1 -7
View File
@@ -1342,13 +1342,7 @@ class QuestionAnswerFilterForm(forms.Form):
opqs = opqs.filter(canceled=False)
if fdata.get("item", "") != "":
i = fdata.get("item", "")
if '-' in i:
opqs = opqs.filter(
item_id=i.split('-')[0],
variation_id=i.split('-')[1],
)
else:
opqs = opqs.filter(item_id=i)
opqs = opqs.filter(item_id__in=(i,))
return opqs
@@ -66,7 +66,6 @@
<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 "pretixbase/js/addressform.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/errors.js" %}"></script>
{% endcompress %}
{{ html_head|safe }}
+5 -2
View File
@@ -1148,8 +1148,11 @@ class EventLive(EventPermissionRequiredMixin, TemplateView):
if request.POST.get("delete") == "yes":
try:
with transaction.atomic():
for order in request.event.orders.filter(testmode=True):
order.gracefully_delete(user=self.request.user)
Order.gracefully_delete_bulk(
request.event,
request.event.orders.filter(testmode=True),
user=self.request.user
)
except ProtectedError:
messages.error(self.request, _('An order could not be deleted as some constraints (e.g. data '
'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.format import SafeFormatter, format_map
from pretix.helpers.hierarkey import clean_filename
from pretix.helpers.iter import chunked_iterable
from pretix.helpers.json import CustomJSONEncoder
from pretix.helpers.safedownload import check_token
from pretix.presale.signals import question_form_fields
@@ -240,7 +241,7 @@ class BaseOrderBulkActionView(OrderSearchMixin, EventPermissionRequiredMixin, As
raise NotImplementedError()
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()
orders_with_successful_action = 0
for i, o in enumerate(qs):
@@ -394,9 +395,21 @@ class OrderDeleteBulkActionView(BaseOrderBulkActionView):
testmode=True,
)
def execute_single(self, instance, form: forms.Form):
instance.gracefully_delete(user=self.request.user)
return True
def execute_bulk(self, queryset: QuerySet, form: forms.Form):
qs = self.allowed_for(self.get_queryset())
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):
@@ -270,18 +270,18 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
def _transaction_group_header_label(self):
return _("Event") + " / " + _("Product")
def _transaction_group(self, form_data, r):
def _transaction_group_label(self, form_data, r):
if not self.is_multievent and not form_data.get("split_subevents"):
return None, None
return None
if r.get("subevent_id"):
return "{} - {} ({}) [{}]".format(
r["order__event__name"],
r["subevent__name"],
date_format(r["subevent__date_from"]),
r["order__event__slug"]
), f"subevent-{r['subevent_id']}"
)
else:
return "{} [{}]".format(r["order__event__name"], r["order__event__slug"]), f"event-{r['order__event__slug']}"
return "{} [{}]".format(r["order__event__name"], r["order__event__slug"])
def _transaction_row_label(self, r):
if r["item_id"]:
@@ -343,17 +343,17 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
last_group = None
last_group_head_idx = 0
for r in qs:
group_label, group_id = self._transaction_group(form_data, r)
e = self._transaction_group_label(form_data, r)
if group_id != last_group:
if last_group_head_idx > 0 and group_id is not None:
if e != last_group:
if last_group_head_idx > 0 and e is not None:
tdata[last_group_head_idx][4] = Paragraph(money_filter(sum_price_by_group - sum_tax_by_group, currency), tstyle_bold_right),
tdata[last_group_head_idx][5] = Paragraph(money_filter(sum_tax_by_group, currency), tstyle_bold_right),
tdata[last_group_head_idx][6] = Paragraph(money_filter(sum_price_by_group, currency), tstyle_bold_right),
tdata.append(
[
FontFallbackParagraph(
group_label,
e,
tstyle_bold,
),
"",
@@ -367,7 +367,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
tstyledata.append(
("SPAN", (0, len(tdata) - 1), (3, len(tdata) - 1)),
)
last_group = group_id
last_group = e
last_group_head_idx = len(tdata) - 1
sum_price_by_group = Decimal("0.00")
sum_tax_by_group = Decimal("0.00")
@@ -9,7 +9,7 @@
{% load anonymize_email %}
{% block thetitle %}
{% if messages %}
{{ messages|join:" " }} ::
{{ messages|join:" " }} ::
{% endif %}
{% block title %}{% endblock %}{% if request.resolver_match.url_name != "event.index" %} :: {% endif %}{{ event.name }}
{% endblock %}
@@ -1,7 +1,6 @@
{% extends "pretixpresale/event/base.html" %}
{% load i18n %}
{% load l10n %}
{% load static %}
{% load eventurl %}
{% load cache_large %}
{% load money %}
@@ -40,7 +39,6 @@
{% else %}
<meta property="og:url" content="{% abseventurl request.event "presale:event.index" %}" />
{% endif %}
<script type="text/javascript" src="{% static "pretixpresale/js/csrfcookieretry.js" %}"></script>
{% endblock %}
{% block content %}
@@ -6,7 +6,6 @@
{% load money %}
{% load expiresformat %}
{% load eventurl %}
{% load static %}
{% load phone_format %}
{% load rich_text %}
{% load getitem %}
@@ -23,10 +22,6 @@
{% endif %}
{% trans "Order details" %}
{% endblock %}
{% block custom_header %}
{{ block.super }}
<script type="text/javascript" src="{% static "pretixpresale/js/csrfcookieretry.js" %}"></script>
{% endblock %}
{% block content %}
{% if "thanks" in request.GET or "paid" in request.GET %}
<div class="thank-you">
@@ -22,5 +22,4 @@
<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/deanonymize_email.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/errors.js" %}"></script>
{% endcompress %}
+2 -3
View File
@@ -174,16 +174,15 @@ function async_task_error(jqXHR, textStatus, errorThrown) {
var respdom = $(jqXHR.responseText);
var c = respdom.filter('.container');
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) {
// This is a failed form validation, let's just use it
async_task_replace_page("#page-wrapper", respdom.find("#page-wrapper").html());
} else {
async_task_replace_page("body", jqXHR.responseText.substring(
jqXHR.responseText.indexOf("<body"),
jqXHR.responseText.indexOf("</body")
));
document.dispatchEvent(new Event("pretix:async-task-error"))
}
} else if (c.length > 0) {
+5 -11
View File
@@ -1,11 +1,5 @@
['DOMContentLoaded', 'pretix:async-task-error'].forEach(function (ev) {
document.addEventListener(ev, function () {
document.querySelectorAll('#goback, #reload').forEach(function (element) {
const regularLoad = ev === 'DOMContentLoaded' && element.id === 'goback';
element.addEventListener('click', regularLoad
? () => window.history.back()
: () => window.location.reload()
);
});
});
});
document.getElementById('goback').onclick =
function() {window.history.back()};
document.getElementById('reload').onclick =
function() {window.location.reload(true)};
@@ -1,15 +0,0 @@
document.addEventListener("DOMContentLoaded", () => {
const COOKIE_NAME = "__Host-pretix_csrftoken";
const RELOAD_FLAG = "csrfReloadPerformed";
const hasCookie = document.cookie
.split("; ")
.some((c) => c.startsWith(COOKIE_NAME + "="));
if (!hasCookie && !sessionStorage.getItem(RELOAD_FLAG)) {
sessionStorage.setItem(RELOAD_FLAG, "1");
location.reload();
} else if (hasCookie && sessionStorage.getItem(RELOAD_FLAG)) {
sessionStorage.removeItem(RELOAD_FLAG);
}
});
@@ -2051,16 +2051,9 @@ var shared_root_methods = {
this.$root.set_cart_id(data.cart_id);
this.$root.overlay.frame_loading = false;
callback()
}, (xhr, data) => {
if (xhr.status === 429 && typeof xhr.responseURL !== "undefined") {
this.$root.overlay.error_message = strings['cart_error_429'];
this.$root.overlay.frame_loading = false;
this.$root.overlay.error_url_after = this.$root.newTabTarget;
this.$root.overlay.error_url_after_new_tab = true;
} else {
}, () => {
this.$root.overlay.error_message = strings['cart_error'];
this.$root.overlay.frame_loading = false;
}
})
},
get_cart_id: function() {
@@ -2149,14 +2142,7 @@ var shared_root_computed = {
if (this.subevent) {
target = this.target_url + this.subevent + '/';
}
var parameters = this.$root.consent_parameter
if (this.$root.additionalURLParams) {
parameters += `&${this.$root.additionalURLParams}`
}
if (parameters) {
target += '?' + parameters.replace(/^&/, '')
}
return target
return target;
},
useIframe: function () {
if (window.crossOriginIsolated === true) {
@@ -162,15 +162,7 @@ export function createWidgetStore (config: {
return params.toString()
},
newTabTarget (): string {
let url = this.subevent ? `${this.targetUrl}${this.subevent}/` : this.targetUrl
let parameters = this.consentParameter
if (this.additionalURLParams) {
parameters += `&${this.additionalURLParams}`
}
if (parameters) {
url += '?' + parameters.replace(/^&/, '')
}
return url
return this.subevent ? `${this.targetUrl}${this.subevent}/` : this.targetUrl
},
formTarget (): string {
const isFirefox = navigator.userAgent.toLowerCase().includes('firefox')
@@ -449,7 +441,6 @@ export function createWidgetStore (config: {
this.overlay.frameLoading = false
this.overlay.errorUrlAfter = this.newTabTarget
this.overlay.errorUrlAfterNewTab = true
return
} else if (e.status === 405) {
// Likely a redirect!
this.targetUrl = e.responseUrl.substring(0, e.responseUrl.indexOf('/cart/add') - 18)
@@ -469,18 +460,11 @@ export function createWidgetStore (config: {
this.overlay.frameLoading = true
const data = await createCart(url)
this.setCartId(data.cart_id)
return true
} catch (e) {
if (e instanceof ApiError && e.status === 429) {
this.overlay.errorMessage = STRINGS.cart_error_429
this.overlay.frameLoading = false
this.overlay.errorUrlAfter = this.newTabTarget
this.overlay.errorUrlAfterNewTab = true
} else if (e instanceof ApiError && (e.status === 200 || (e.status >= 400 && e.status < 500))) {
if (e instanceof ApiError && (e.status === 200 || (e.status >= 400 && e.status < 500))) {
this.overlay.errorMessage = STRINGS.cart_error
this.overlay.frameLoading = false
}
return false
}
},
redeem (voucherCode: string, event?: Event) {
@@ -500,7 +484,7 @@ export function createWidgetStore (config: {
async resume () {
if (!this.cartId && this.keepCart) {
// create an empty cart whose id we can persist
if (!await this.createCart()) return
await this.createCart()
}
let redirectUrl = `${this.targetUrl}w/${globalWidgetId}/`
if (this.subevent && this.isButton && this.items.length === 0) {