diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py index f76adbcd75..c86af15268 100644 --- a/src/pretix/base/models/event.py +++ b/src/pretix/base/models/event.py @@ -1113,12 +1113,28 @@ class SubEvent(EventMixin, LoggedModel): if self.event and clear_cache: self.event.cache.clear() + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__original_dates = (self.date_from, self.date_to) + def save(self, *args, **kwargs): + from .orders import Order + clear_cache = kwargs.pop('clear_cache', False) super().save(*args, **kwargs) if self.event and clear_cache: self.event.cache.clear() + if (self.date_from, self.date_to) != self.__original_dates: + """ + This is required to guarantee a synchronization invariant of our scanning apps. + Our syncing apps throw away order records of subevents more than X days ago, since + they are not interesting for ticket scanning and pose a performance hazard. However, + the app needs to know when a subevent is moved to a date in the future, since that + might require it to re-download and re-store the orders. + """ + Order.objects.filter(all_positions__subevent=self).update(last_modified=now()) + @staticmethod def clean_items(event, items): for item in items: diff --git a/src/tests/base/test_models.py b/src/tests/base/test_models.py index 6378a11ba8..2c2cfa8776 100644 --- a/src/tests/base/test_models.py +++ b/src/tests/base/test_models.py @@ -1,5 +1,6 @@ import datetime import sys +import time from datetime import date, timedelta from decimal import Decimal @@ -2566,3 +2567,38 @@ def test_question_answer_validation_multiple_choice(): assert q.clean_answer([o1.pk, o3.pk + 1000]) == [o1] with pytest.raises(ValidationError): assert q.clean_answer([o1.pk, 'FOO']) == [o1] + + +@pytest.mark.django_db +def test_subevent_date_updates_order_date(): + # When the date of a subevent changes, all orders need to get a bumped modification date to hold + # a required invariant of the libpretixsync synchronization approach. + organizer = Organizer.objects.create(name='Dummy', slug='dummy') + with scope(organizer=organizer): + event = Event.objects.create( + organizer=organizer, name='Dummy', slug='dummy', + date_from=now(), date_to=now() - timedelta(hours=1), has_subevents=True + ) + item1 = Item.objects.create(event=event, name="Ticket", default_price=23, admission=True) + se1 = event.subevents.create(date_from=now(), name="SE 1") + se2 = event.subevents.create(date_from=now(), name="SE 2") + + order1 = Order.objects.create(event=event, status=Order.STATUS_PAID, expires=now() + timedelta(days=3), total=6) + OrderPosition.objects.create(order=order1, item=item1, subevent=se1, price=2) + order2 = Order.objects.create(event=event, status=Order.STATUS_PAID, expires=now() + timedelta(days=3), total=6) + OrderPosition.objects.create(order=order2, item=item1, subevent=se2, price=2) + + o1lm = order1.last_modified + o2lm = order2.last_modified + + time.sleep(1) + se1.date_from += timedelta(days=2) + se1.save() + se2.name = "foo" + se2.save() + + order1.refresh_from_db() + order2.refresh_from_db() + + assert order1.last_modified > o1lm + assert order2.last_modified == o2lm