mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
Time machine mode [Z#23129725] (#3961)
Allows organizers to test their shop as if it were a different date and time. Implemented using a time_machine_now() function which is used instead of regular now(), which can overlay the real date time with a value from a ContextVar, assigned from a session value in EventMiddleware. For more information, see doc/development/implementation/timemachine.rst --------- Co-authored-by: Richard Schreiber <schreiber@rami.io> Co-authored-by: Raphael Michel <michel@rami.io>
This commit is contained in:
@@ -68,6 +68,7 @@ from i18nfield.fields import I18nCharField, I18nTextField
|
||||
from pretix.base.models.base import LoggedModel
|
||||
from pretix.base.models.fields import MultiStringField
|
||||
from pretix.base.reldate import RelativeDateWrapper
|
||||
from pretix.base.timemachine import time_machine_now
|
||||
from pretix.base.validators import EventSlugBanlistValidator
|
||||
from pretix.helpers.database import GroupConcat
|
||||
from pretix.helpers.daterange import daterange
|
||||
@@ -235,7 +236,7 @@ class EventMixin:
|
||||
if not self.settings.waiting_list_enabled:
|
||||
return False
|
||||
if self.settings.waiting_list_auto_disable:
|
||||
return self.settings.waiting_list_auto_disable.datetime(self) > now()
|
||||
return self.settings.waiting_list_auto_disable.datetime(self) > time_machine_now()
|
||||
return True
|
||||
|
||||
@property
|
||||
@@ -244,11 +245,11 @@ class EventMixin:
|
||||
Is true, when ``presale_end`` is set and in the past.
|
||||
"""
|
||||
if self.effective_presale_end:
|
||||
return now() > self.effective_presale_end
|
||||
return time_machine_now() > self.effective_presale_end
|
||||
elif self.date_to:
|
||||
return now() > self.date_to
|
||||
return time_machine_now() > self.date_to
|
||||
else:
|
||||
return now().astimezone(self.timezone).date() > self.date_from.astimezone(self.timezone).date()
|
||||
return time_machine_now().astimezone(self.timezone).date() > self.date_from.astimezone(self.timezone).date()
|
||||
|
||||
@property
|
||||
def effective_presale_start(self):
|
||||
@@ -268,7 +269,7 @@ class EventMixin:
|
||||
Is true, when ``presale_end`` is not set or in the future and ``presale_start`` is not
|
||||
set or in the past.
|
||||
"""
|
||||
if self.effective_presale_start and now() < self.effective_presale_start:
|
||||
if self.effective_presale_start and time_machine_now() < self.effective_presale_start:
|
||||
return False
|
||||
return not self.presale_has_ended
|
||||
|
||||
@@ -316,11 +317,11 @@ class EventMixin:
|
||||
q_variation = (
|
||||
Q(active=True)
|
||||
& Q(sales_channels__contains=channel)
|
||||
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
|
||||
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
|
||||
& Q(Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()))
|
||||
& Q(Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()))
|
||||
& Q(item__active=True)
|
||||
& Q(Q(item__available_from__isnull=True) | Q(item__available_from__lte=now()))
|
||||
& Q(Q(item__available_until__isnull=True) | Q(item__available_until__gte=now()))
|
||||
& Q(Q(item__available_from__isnull=True) | Q(item__available_from__lte=time_machine_now()))
|
||||
& Q(Q(item__available_until__isnull=True) | Q(item__available_until__gte=time_machine_now()))
|
||||
& Q(Q(item__category__isnull=True) | Q(item__category__is_addon=False))
|
||||
& Q(item__sales_channels__contains=channel)
|
||||
& Q(item__require_bundling=False)
|
||||
@@ -695,7 +696,7 @@ class Event(EventMixin, LoggedModel):
|
||||
@property
|
||||
def presale_has_ended(self):
|
||||
if self.has_subevents:
|
||||
return self.presale_end and now() > self.presale_end
|
||||
return self.presale_end and time_machine_now() > self.presale_end
|
||||
else:
|
||||
return super().presale_has_ended
|
||||
|
||||
@@ -1188,8 +1189,8 @@ class Event(EventMixin, LoggedModel):
|
||||
)
|
||||
).filter(
|
||||
Q(active=True) & Q(is_public=True) & (
|
||||
Q(Q(date_to__isnull=True) & Q(date_from__gte=now() - timedelta(hours=24)))
|
||||
| Q(date_to__gte=now() - timedelta(hours=24))
|
||||
Q(Q(date_to__isnull=True) & Q(date_from__gte=time_machine_now() - timedelta(hours=24)))
|
||||
| Q(date_to__gte=time_machine_now() - timedelta(hours=24))
|
||||
)
|
||||
) # order_by doesn't make sense with I18nField
|
||||
if ordering in ("date_ascending", "date_descending"):
|
||||
@@ -1509,7 +1510,7 @@ class SubEvent(EventMixin, LoggedModel):
|
||||
disabled_items=Coalesce(
|
||||
Subquery(
|
||||
SubEventItem.objects.filter(
|
||||
Q(disabled=True) | Q(available_from__gt=now()) | Q(available_until__lt=now()),
|
||||
Q(disabled=True) | Q(available_from__gt=time_machine_now()) | Q(available_until__lt=time_machine_now()),
|
||||
subevent=OuterRef('pk'),
|
||||
).order_by().values('subevent').annotate(items=GroupConcat('item_id', delimiter=',')).values('items'),
|
||||
output_field=models.TextField(),
|
||||
@@ -1520,7 +1521,7 @@ class SubEvent(EventMixin, LoggedModel):
|
||||
disabled_vars=Coalesce(
|
||||
Subquery(
|
||||
SubEventItemVariation.objects.filter(
|
||||
Q(disabled=True) | Q(available_from__gt=now()) | Q(available_until__lt=now()),
|
||||
Q(disabled=True) | Q(available_from__gt=time_machine_now()) | Q(available_until__lt=time_machine_now()),
|
||||
subevent=OuterRef('pk'),
|
||||
).order_by().values('subevent').annotate(items=GroupConcat('variation_id', delimiter=',')).values('items'),
|
||||
output_field=models.TextField(),
|
||||
|
||||
@@ -55,7 +55,7 @@ from django.db.models import Q
|
||||
from django.utils import formats
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import is_naive, make_aware, now
|
||||
from django.utils.timezone import is_naive, make_aware
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_countries.fields import Country
|
||||
from django_scopes import ScopedManager
|
||||
@@ -65,6 +65,7 @@ from pretix.base.models import fields
|
||||
from pretix.base.models.base import LoggedModel
|
||||
from pretix.base.models.fields import MultiStringField
|
||||
from pretix.base.models.tax import TaxedPrice
|
||||
from pretix.base.timemachine import time_machine_now
|
||||
|
||||
from ...helpers.images import ImageSizeValidator
|
||||
from ..media import MEDIA_TYPES
|
||||
@@ -192,7 +193,7 @@ class SubEventItem(models.Model):
|
||||
self.subevent.event.cache.clear()
|
||||
|
||||
def is_available(self, now_dt: datetime=None) -> bool:
|
||||
now_dt = now_dt or now()
|
||||
now_dt = now_dt or time_machine_now()
|
||||
if self.disabled:
|
||||
return False
|
||||
if self.available_from and self.available_from > now_dt:
|
||||
@@ -248,7 +249,7 @@ class SubEventItemVariation(models.Model):
|
||||
self.subevent.event.cache.clear()
|
||||
|
||||
def is_available(self, now_dt: datetime=None) -> bool:
|
||||
now_dt = now_dt or now()
|
||||
now_dt = now_dt or time_machine_now()
|
||||
if self.disabled:
|
||||
return False
|
||||
if self.available_from and self.available_from > now_dt:
|
||||
@@ -263,8 +264,8 @@ def filter_available(qs, channel='web', voucher=None, allow_addons=False):
|
||||
# IMPORTANT: If this is updated, also update the ItemVariation query
|
||||
# in models/event.py: EventMixin.annotated()
|
||||
Q(active=True)
|
||||
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()) | Q(available_from_mode='info'))
|
||||
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()) | Q(available_until_mode='info'))
|
||||
& Q(Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()) | Q(available_from_mode='info'))
|
||||
& Q(Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()) | Q(available_until_mode='info'))
|
||||
& Q(sales_channels__contains=channel) & Q(require_bundling=False)
|
||||
)
|
||||
if not allow_addons:
|
||||
@@ -782,7 +783,7 @@ class Item(LoggedModel):
|
||||
return t
|
||||
|
||||
def is_available_by_time(self, now_dt: datetime=None) -> bool:
|
||||
now_dt = now_dt or now()
|
||||
now_dt = now_dt or time_machine_now()
|
||||
if self.available_from and self.available_from > now_dt:
|
||||
return False
|
||||
if self.available_until and self.available_until < now_dt:
|
||||
@@ -794,13 +795,13 @@ class Item(LoggedModel):
|
||||
Returns whether this item is available according to its ``active`` flag
|
||||
and its ``available_from`` and ``available_until`` fields
|
||||
"""
|
||||
now_dt = now_dt or now()
|
||||
now_dt = now_dt or time_machine_now()
|
||||
if not self.active or not self.is_available_by_time(now_dt):
|
||||
return False
|
||||
return True
|
||||
|
||||
def unavailability_reason(self, now_dt: datetime=None, has_voucher=False, subevent=None) -> Optional[str]:
|
||||
now_dt = now_dt or now()
|
||||
now_dt = now_dt or time_machine_now()
|
||||
subevent_item = subevent and subevent.item_overrides.get(self.pk)
|
||||
if not self.active:
|
||||
return 'active'
|
||||
@@ -957,11 +958,11 @@ class Item(LoggedModel):
|
||||
return self.validity_fixed_from, self.validity_fixed_until
|
||||
elif self.validity_mode == Item.VALIDITY_MODE_DYNAMIC:
|
||||
tz = override_tz or self.event.timezone
|
||||
requested_start = requested_start or now()
|
||||
requested_start = requested_start or time_machine_now()
|
||||
if enforce_start_limit and not self.validity_dynamic_start_choice:
|
||||
requested_start = now()
|
||||
requested_start = time_machine_now()
|
||||
if enforce_start_limit and self.validity_dynamic_start_choice_day_limit is not None:
|
||||
requested_start = min(requested_start, now() + timedelta(days=self.validity_dynamic_start_choice_day_limit))
|
||||
requested_start = min(requested_start, time_machine_now() + timedelta(days=self.validity_dynamic_start_choice_day_limit))
|
||||
|
||||
valid_until = requested_start.astimezone(tz)
|
||||
|
||||
@@ -1290,7 +1291,7 @@ class ItemVariation(models.Model):
|
||||
return ItemVariation.objects.filter(item=self.item).count() == 1
|
||||
|
||||
def is_available_by_time(self, now_dt: datetime=None) -> bool:
|
||||
now_dt = now_dt or now()
|
||||
now_dt = now_dt or time_machine_now()
|
||||
if self.available_from and self.available_from > now_dt:
|
||||
return False
|
||||
if self.available_until and self.available_until < now_dt:
|
||||
@@ -1302,13 +1303,13 @@ class ItemVariation(models.Model):
|
||||
Returns whether this item is available according to its ``active`` flag
|
||||
and its ``available_from`` and ``available_until`` fields
|
||||
"""
|
||||
now_dt = now_dt or now()
|
||||
now_dt = now_dt or time_machine_now()
|
||||
if not self.active or not self.is_available_by_time(now_dt):
|
||||
return False
|
||||
return True
|
||||
|
||||
def unavailability_reason(self, now_dt: datetime=None, has_voucher=False, subevent=None) -> Optional[str]:
|
||||
now_dt = now_dt or now()
|
||||
now_dt = now_dt or time_machine_now()
|
||||
subevent_var = subevent and subevent.var_overrides.get(self.pk)
|
||||
if not self.active:
|
||||
return 'active'
|
||||
|
||||
@@ -23,7 +23,6 @@ from django.db import models
|
||||
from django.db.models import Count, OuterRef, Subquery, Value
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes import ScopedManager, scopes_disabled
|
||||
from i18nfield.fields import I18nCharField
|
||||
@@ -31,6 +30,7 @@ from i18nfield.fields import I18nCharField
|
||||
from pretix.base.models import Customer
|
||||
from pretix.base.models.base import LoggedModel
|
||||
from pretix.base.models.organizer import Organizer
|
||||
from pretix.base.timemachine import time_machine_now
|
||||
from pretix.helpers.names import build_name
|
||||
|
||||
|
||||
@@ -165,13 +165,13 @@ class Membership(models.Model):
|
||||
|
||||
def is_valid(self, ev=None, ticket_valid_from=None, valid_from_not_chosen=False):
|
||||
if valid_from_not_chosen:
|
||||
return not self.canceled and self.date_end >= now()
|
||||
return not self.canceled and self.date_end >= time_machine_now()
|
||||
elif ticket_valid_from:
|
||||
dt = ticket_valid_from
|
||||
elif ev:
|
||||
dt = ev.date_from
|
||||
else:
|
||||
dt = now()
|
||||
dt = time_machine_now()
|
||||
|
||||
return not self.canceled and dt >= self.date_start and dt <= self.date_end
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ from pretix.base.models import Customer, User
|
||||
from pretix.base.reldate import RelativeDateWrapper
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.base.signals import allow_ticket_download, order_gracefully_delete
|
||||
from pretix.base.timemachine import time_machine_now
|
||||
|
||||
from ...helpers import OF_SELF
|
||||
from ...helpers.countries import CachedCountries, FastCountryField
|
||||
@@ -681,7 +682,7 @@ class Order(LockModel, LoggedModel):
|
||||
for op in positions:
|
||||
if op.issued_gift_cards.all():
|
||||
return False
|
||||
if self.user_change_deadline and now() > self.user_change_deadline:
|
||||
if self.user_change_deadline and time_machine_now() > self.user_change_deadline:
|
||||
return False
|
||||
|
||||
return (
|
||||
@@ -713,7 +714,7 @@ class Order(LockModel, LoggedModel):
|
||||
return False
|
||||
if op.granted_memberships.with_usages().filter(usages__gt=0):
|
||||
return False
|
||||
if self.user_cancel_deadline and now() > self.user_cancel_deadline:
|
||||
if self.user_cancel_deadline and time_machine_now() > self.user_cancel_deadline:
|
||||
return False
|
||||
|
||||
if self.status == Order.STATUS_PAID:
|
||||
@@ -854,7 +855,7 @@ class Order(LockModel, LoggedModel):
|
||||
return False
|
||||
|
||||
modify_deadline = self.modify_deadline
|
||||
if modify_deadline is not None and now() > modify_deadline:
|
||||
if modify_deadline is not None and time_machine_now() > modify_deadline:
|
||||
return False
|
||||
|
||||
positions = list(
|
||||
@@ -906,7 +907,7 @@ class Order(LockModel, LoggedModel):
|
||||
return self.event.settings.ticket_download and (
|
||||
self.event.settings.ticket_download_date is None
|
||||
or self.ticket_download_date is None
|
||||
or now() > self.ticket_download_date
|
||||
or time_machine_now() > self.ticket_download_date
|
||||
) and (
|
||||
self.status == Order.STATUS_PAID
|
||||
or (
|
||||
@@ -978,7 +979,7 @@ class Order(LockModel, LoggedModel):
|
||||
return error_messages['require_approval']
|
||||
term_last = self.payment_term_last
|
||||
if term_last and not ignore_date:
|
||||
if now() > term_last:
|
||||
if time_machine_now() > term_last:
|
||||
return error_messages['late_lastdate']
|
||||
|
||||
if self.status == self.STATUS_PENDING:
|
||||
@@ -1001,7 +1002,7 @@ class Order(LockModel, LoggedModel):
|
||||
'voucher_budget': _('The voucher "{voucher}" no longer has sufficient budget.'),
|
||||
'voucher_usages': _('The voucher "{voucher}" has been used in the meantime.'),
|
||||
}
|
||||
now_dt = now_dt or now()
|
||||
now_dt = now_dt or time_machine_now()
|
||||
positions = list(self.positions.all().select_related('item', 'variation', 'seat', 'voucher'))
|
||||
quota_cache = {}
|
||||
v_budget = {}
|
||||
@@ -2575,9 +2576,9 @@ class OrderPosition(AbstractPosition):
|
||||
if cartpos.item.validity_mode:
|
||||
valid_from, valid_until = cartpos.item.compute_validity(
|
||||
requested_start=(
|
||||
max(cartpos.requested_valid_from, now())
|
||||
max(cartpos.requested_valid_from, time_machine_now())
|
||||
if cartpos.requested_valid_from and cartpos.item.validity_dynamic_start_choice
|
||||
else now()
|
||||
else time_machine_now()
|
||||
),
|
||||
enforce_start_limit=True,
|
||||
override_tz=order.event.timezone,
|
||||
@@ -3103,9 +3104,9 @@ class CartPosition(AbstractPosition):
|
||||
def predicted_validity(self):
|
||||
return self.item.compute_validity(
|
||||
requested_start=(
|
||||
max(self.requested_valid_from, now())
|
||||
max(self.requested_valid_from, time_machine_now())
|
||||
if self.requested_valid_from and self.item.validity_dynamic_start_choice
|
||||
else now()
|
||||
else time_machine_now()
|
||||
),
|
||||
override_tz=self.event.timezone,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user