Compare commits

...

6 Commits

Author SHA1 Message Date
Raphael Michel
70a304d37c Overview export: Allow to skip empty lines (Z#23219200) 2026-01-20 12:31:34 +01:00
Raphael Michel
815e31d9a0 Resolve syntax warning in Pyton 3.14 2026-01-20 12:15:49 +01:00
Lukas Bockstaller
ed618f2f32 add tiered availability by time (Z#23204747) (#5737)
* add tiered availability by time

* replace bitwise operator

* rephrase help text
2026-01-20 10:32:17 +01:00
Lukas Bockstaller
a900e11ce0 Reduce queries for waitinglist autoassign n+1 (PRETIXEU-BJJ) (#5819)
* baseline of 574 queries

* reuse event from wle for locked_wle

reduces amount of queries to 556

* keep event accross refresh from db

drops queries from 556 to 471, halving the amount of queries for direct fetches for the event

* make numbers of queries reproducible by prewarming ContentTypeCache

* fix oversight

* correct number of queries to 335

* remove debug tooling

* remove assert_num_queries
2026-01-20 10:31:58 +01:00
Richard Schreiber
112d5da792 Localize state names (#5744)
* Localize state names in js-helper

* localize statename in address-confirm

* add localized state_name to AbstractPosition and AttendeeProfile

* use state_for_address in order export
2026-01-20 10:13:20 +01:00
Richard Schreiber
ceb2e13d27 Remove autofocus from only button in cart-extend confirm-dialog (#5821) 2026-01-20 09:53:16 +01:00
12 changed files with 79 additions and 17 deletions

View File

@@ -364,7 +364,7 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.city,
order.invoice_address.country if order.invoice_address.country else
order.invoice_address.country_old,
order.invoice_address.state,
order.invoice_address.state_for_address,
order.invoice_address.custom_field,
order.invoice_address.vat_id,
]
@@ -515,7 +515,7 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.city,
order.invoice_address.country if order.invoice_address.country else
order.invoice_address.country_old,
order.invoice_address.state,
order.invoice_address.state_for_address,
order.invoice_address.vat_id,
]
except InvoiceAddress.DoesNotExist:
@@ -732,7 +732,7 @@ class OrderListExporter(MultiSheetListExporter):
op.zipcode or '',
op.city or '',
op.country if op.country else '',
op.state or '',
op.state_for_address or '',
op.voucher.code if op.voucher else '',
op.pseudonymization_id,
op.secret,
@@ -797,7 +797,7 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.city,
order.invoice_address.country if order.invoice_address.country else
order.invoice_address.country_old,
order.invoice_address.state,
order.invoice_address.state_for_address,
order.invoice_address.vat_id,
]
except InvoiceAddress.DoesNotExist:

View File

@@ -349,7 +349,7 @@ class AttendeeProfile(models.Model):
def state_name(self):
sd = pycountry.subdivisions.get(code='{}-{}'.format(self.country, self.state))
if sd:
return sd.name
return _(sd.name)
return self.state
@property

View File

@@ -594,10 +594,11 @@ class Item(LoggedModel):
on_delete=models.SET_NULL,
verbose_name=_("Only show after sellout of"),
help_text=_("If you select a product here, this product will only be shown when that product is "
"sold out. If combined with the option to hide sold-out products, this allows you to "
"swap out products for more expensive ones once the cheaper option is sold out. There might "
"be a short period in which both products are visible while all tickets of the referenced "
"product are reserved, but not yet sold.")
"no longer available. This will happen either because the other product has sold out or because "
"the time is outside of the sales window for the other product. If combined with the option "
"to hide sold-out products, this allows you to swap out products for more expensive ones once "
"the cheaper option is sold out. There might be a short period in which both products are visible "
"while all tickets of the referenced product are reserved, but not yet sold.")
)
hidden_if_item_available_mode = models.CharField(
choices=UNAVAIL_MODES,

View File

@@ -1675,7 +1675,7 @@ class AbstractPosition(RoundingCorrectionMixin, models.Model):
def state_name(self):
sd = pycountry.subdivisions.get(code='{}-{}'.format(self.country, self.state))
if sd:
return sd.name
return _(sd.name)
return self.state
@property
@@ -3480,7 +3480,7 @@ class InvoiceAddress(models.Model):
def state_name(self):
sd = pycountry.subdivisions.get(code='{}-{}'.format(self.country, self.state))
if sd:
return sd.name
return _(sd.name)
return self.state
@property

View File

@@ -159,6 +159,7 @@ class WaitingListEntry(LoggedModel):
if availability[1] is None or availability[1] < 1:
raise WaitingListException(_('This product is currently not available.'))
event = self.event
ev = self.subevent or self.event
if ev.seat_category_mappings.filter(product=self.item).exists():
# Generally, we advertise the waiting list to be based on quotas only. This makes it dangerous
@@ -191,6 +192,7 @@ class WaitingListEntry(LoggedModel):
with transaction.atomic():
locked_wle = WaitingListEntry.objects.select_for_update(of=OF_SELF).get(pk=self.pk)
locked_wle.event = event
if locked_wle.voucher:
raise WaitingListException(_('A voucher has already been sent to this person.'))
e = locked_wle.email
@@ -227,6 +229,7 @@ class WaitingListEntry(LoggedModel):
locked_wle.save()
self.refresh_from_db()
self.event = event
with language(self.locale, self.event.settings.region):
self.send_mail(

View File

@@ -112,7 +112,8 @@ def dictsum(*dicts) -> dict:
def order_overview(
event: Event, subevent: SubEvent=None, date_filter='', date_from=None, date_until=None, fees=False,
admission_only=False, base_qs=None, base_fees_qs=None, subevent_date_from=None, subevent_date_until=None
admission_only=False, base_qs=None, base_fees_qs=None, subevent_date_from=None, subevent_date_until=None,
skip_empty_lines=False,
) -> Tuple[List[Tuple[ItemCategory, List[Item]]], Dict[str, Tuple[Decimal, Decimal]]]:
items = event.items.all().select_related(
'category', # for re-grouping
@@ -205,13 +206,21 @@ def order_overview(
for l in states.keys():
var.num[l] = num[l].get((item.id, variid), (0, 0, 0))
var.num['total'] = num['total'].get((item.id, variid), (0, 0, 0))
var._skip = all(v[0] == 0 for v in var.num.values())
for l in states.keys():
item.num[l] = tuplesum(var.num[l] for var in item.all_variations)
item.num['total'] = tuplesum(var.num['total'] for var in item.all_variations)
if skip_empty_lines:
item.all_variations = [v for v in item.all_variations if not v._skip]
item._skip = not item.all_variations
else:
for l in states.keys():
item.num[l] = num[l].get((item.id, None), (0, 0, 0))
item.num['total'] = num['total'].get((item.id, None), (0, 0, 0))
item._skip = all(v[0] == 0 for v in item.num.values())
if skip_empty_lines:
items = [i for i in items if not i._skip]
nonecat = ItemCategory(name=_('Uncategorized'))
# Regroup those by category

View File

@@ -82,7 +82,7 @@ def _info(cc):
statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types]
return {
'data': [
{'name': s.name, 'code': s.code[3:]}
{'name': gettext(s.name), 'code': s.code[3:]}
for s in sorted(statelist, key=lambda s: s.name)
],
**info,

View File

@@ -363,7 +363,7 @@ class Forgot(TemplateView):
else:
messages.info(request, _('If the address is registered to valid account, then we have sent you an email containing further instructions.'))
return redirect('control:auth.forgot')
return redirect('control:auth.forgot')
else:
return self.get(request, *args, **kwargs)

View File

@@ -337,7 +337,8 @@ class OverviewReport(Report):
date_until=d_end,
subevent_date_from=sd_start,
subevent_date_until=sd_end,
fees=True
fees=True,
skip_empty_lines=form_data.get("skip_empty_lines")
)
def _table_story(self, doc, form_data, net=False):
@@ -478,6 +479,10 @@ class OverviewReport(Report):
'Use the "Accounting report" in the export section instead.'
))
)
f.fields['skip_empty_lines'] = forms.BooleanField(
label=_("Skip empty lines"),
required=False,
)
return f.fields

View File

@@ -519,7 +519,7 @@
<dialog role="alertdialog" id="cart-extend-confirmation-dialog" class="inline-dialog" aria-labelledby="cart-deadline">
<form method="dialog">
<p>
<button class="btn btn-success" autofocus value="OK">
<button class="btn btn-success" value="OK">
<span role="img" aria-label="{% trans "OK" %}.">
{% icon "check" %}
</span>

View File

@@ -311,7 +311,8 @@ def get_grouped_items(event, *, channel: SalesChannel, subevent=None, voucher=No
)
else:
q = item.hidden_if_item_available.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)
item._dependency_available = q[0] == Quota.AVAILABILITY_OK
time_available = item.hidden_if_item_available.is_available()
item._dependency_available = (q[0] == Quota.AVAILABILITY_OK) and time_available
if item._dependency_available and item.hidden_if_item_available_mode == Item.UNAVAIL_MODE_HIDDEN:
item._remove = True
continue

View File

@@ -46,6 +46,7 @@ from django.core.exceptions import ValidationError
from django.test import TestCase
from django.utils.timezone import now
from django_scopes import scopes_disabled
from freezegun import freeze_time
from tests.base import SoupTest
from tests.testdummy.signals import FoobarSalesChannel
@@ -272,6 +273,48 @@ class ItemDisplayTest(EventTestMixin, SoupTest):
resp = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug))
self.assertNotIn("Early-bird", resp.rendered_content)
def tiered_availability_by_date_and_quota(self, q1_size, q2_size, time_offset, expected_phase):
current_time = now()
with scopes_disabled():
q1 = Quota.objects.create(event=self.event, name='Phase 1', size=q1_size)
item1 = Item.objects.create(
event=self.event,
name='Phase 1',
default_price=0,
available_from=current_time,
available_until=current_time + datetime.timedelta(days=1),
available_from_mode=Item.UNAVAIL_MODE_HIDDEN,
available_until_mode=Item.UNAVAIL_MODE_HIDDEN,
hidden_if_item_available_mode=Item.UNAVAIL_MODE_HIDDEN,
)
q1.items.add(item1)
q2 = Quota.objects.create(event=self.event, name='Phase 2', size=q2_size)
item2 = Item.objects.create(
event=self.event,
name='Phase 2',
default_price=0,
available_from=current_time + datetime.timedelta(days=0),
available_until=current_time + datetime.timedelta(days=2),
available_from_mode=Item.UNAVAIL_MODE_HIDDEN,
available_until_mode=Item.UNAVAIL_MODE_HIDDEN,
hidden_if_item_available_mode=Item.UNAVAIL_MODE_HIDDEN,
hidden_if_item_available=item1
)
q2.items.add(item2)
with freeze_time(current_time + time_offset):
resp = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug))
self.assertIn(expected_phase, resp.rendered_content)
def test_tiered_availability_by_date_and_quota_phase1_available(self):
self.tiered_availability_by_date_and_quota(1, 1, datetime.timedelta(seconds=1), "Phase 1")
def test_tiered_availability_by_date_and_quota_phase1_sold_out(self):
self.tiered_availability_by_date_and_quota(0, 1, datetime.timedelta(seconds=1), "Phase 2")
def test_tiered_availability_by_date_and_quota_phase1_timed_out(self):
self.tiered_availability_by_date_and_quota(1, 1, datetime.timedelta(days=1, hours=1), "Phase 2")
def test_subevents_inactive_unknown(self):
self.event.has_subevents = True
self.event.save()