Compare commits

...

3 Commits

Author SHA1 Message Date
Raphael Michel
4142a2b690 Orders API: Fix race condition in voucher redemption (Z#23230391)
The old code relied on the `Voucher.redeemed` value obtained *before*
the lock was taken, not afterwards.

The change in services/orders.py is functionally pointless, but it makes
the pattern of "fill availability only after lock" clearer and might
avoid introducing similar bugs in the future.
2026-04-08 15:07:34 +02:00
Raphael Michel
221cbd15ab [SECURITY] API: Add missing event filter for check-ins 2026-04-08 13:57:55 +02:00
Lukas Bockstaller
5c7104634e Order import: handle mixed endings of last line (Z#23230806) (#6066)
* handle mixed line endings in import

* formatting
2026-04-08 13:25:38 +02:00
5 changed files with 43 additions and 8 deletions

View File

@@ -1412,6 +1412,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
qa = QuotaAvailability()
qa.queue(*[q for q, d in quota_diff_for_locking.items() if d > 0])
qa.compute()
v_avail = {}
# These are not technically correct as diff use due to the time offset applied above, so let's prevent accidental
# use further down
@@ -1441,11 +1442,13 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
voucher_usage[v] += 1
if voucher_usage[v] > 0:
redeemed_in_carts = CartPosition.objects.filter(
Q(voucher=pos_data['voucher']) & Q(event=self.context['event']) & Q(expires__gte=now_dt)
).exclude(pk__in=[cp.pk for cp in delete_cps])
v_avail = v.max_usages - v.redeemed - redeemed_in_carts.count()
if v_avail < voucher_usage[v]:
if v not in v_avail:
v.refresh_from_db(fields=['redeemed'])
redeemed_in_carts = CartPosition.objects.filter(
Q(voucher=v) & Q(event=self.context['event']) & Q(expires__gte=now_dt)
).exclude(pk__in=[cp.pk for cp in delete_cps])
v_avail[v] = v.max_usages - v.redeemed - redeemed_in_carts.count()
if v_avail[v] < voucher_usage[v]:
errs[i]['voucher'] = [
'The voucher has already been used the maximum number of times.'
]

View File

@@ -1122,7 +1122,7 @@ class CheckinViewSet(viewsets.ReadOnlyModelViewSet):
permission = 'event.orders:read'
def get_queryset(self):
qs = Checkin.all.filter().select_related(
qs = Checkin.all.filter(list__event=self.request.event).select_related(
"position",
"device",
)

View File

@@ -70,6 +70,10 @@ def parse_csv(file, length=None, mode="strict", charset=None):
except ImportError:
charset = file.charset
data = data.decode(charset or "utf-8", mode)
# remove stray linebreaks from the end of the file
data = data.rstrip("\n")
# If the file was modified on a Mac, it only contains \r as line breaks
if '\r' in data and '\n' not in data:
data = data.replace('\r', '\n')

View File

@@ -727,8 +727,6 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
_check_date(event, time_machine_now_dt)
products_seen = Counter()
q_avail = Counter()
v_avail = Counter()
v_usages = Counter()
v_budget = {}
deleted_positions = set()
@@ -793,6 +791,9 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
shared_lock_objects=[event]
)
q_avail = Counter()
v_avail = Counter()
# Check maximum order size
limit = min(int(event.settings.max_items_per_order), settings.PRETIX_MAX_ORDER_SIZE)
if sum(1 for cp in sorted_positions if not cp.addon_to) > limit:

View File

@@ -991,3 +991,30 @@ def test_import_mixed_order_size_consistency(user, event, item):
).get()
assert ('Inconsistent data in row 2: Column Email address contains value "a2@example.com", but for this order, '
'the value has already been set to "a1@example.com".') in str(excinfo.value)
@pytest.mark.django_db
@scopes_disabled()
def test_import_line_endings_mix(event, item, user):
# Ensures import works with mixed file endings.
# See Ticket#23230806 where a file to import ends with \r\n
settings = dict(DEFAULT_SETTINGS)
settings['item'] = 'static:{}'.format(item.pk)
cf = inputfile_factory()
file = cf.file
file.seek(0)
data = file.read()
data = data.replace(b'\n', b'\r')
data = data.rstrip(b'\r\r')
data = data + b'\r\n'
print(data)
cf.file.save("input.csv", ContentFile(data))
cf.save()
import_orders.apply(
args=(event.pk, cf.id, settings, 'en', user.pk)
)
assert event.orders.count() == 3
assert OrderPosition.objects.count() == 3