diff --git a/src/pretix/base/models/items.py b/src/pretix/base/models/items.py index d307ba9c7c..339ad45014 100644 --- a/src/pretix/base/models/items.py +++ b/src/pretix/base/models/items.py @@ -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, diff --git a/src/pretix/presale/views/event.py b/src/pretix/presale/views/event.py index d5c7b35922..2b3b08aaa3 100644 --- a/src/pretix/presale/views/event.py +++ b/src/pretix/presale/views/event.py @@ -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 diff --git a/src/tests/presale/test_event.py b/src/tests/presale/test_event.py index 078dbb7357..2f389bebe1 100644 --- a/src/tests/presale/test_event.py +++ b/src/tests/presale/test_event.py @@ -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()