Compare commits

...

1 Commits

Author SHA1 Message Date
Mira Weller
e4abfb0bdf start implementing time machine mode 2024-03-06 13:18:23 +01:00
12 changed files with 96 additions and 34 deletions

View File

@@ -80,6 +80,15 @@ from .organizer import Organizer, Team
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def annotate_with_time_based_properties(events_or_subevents, now_dt):
print("annotate_with_time_based_properties", now_dt)
for e_s in events_or_subevents:
if e_s:
e_s.presale_is_running = e_s.presale_is_running_by_time(now_dt)
e_s.presale_has_ended = e_s.presale_has_ended_by_time(now_dt)
return events_or_subevents
class EventMixin: class EventMixin:
def clean(self): def clean(self):
if self.presale_start and self.presale_end and self.presale_start > self.presale_end: if self.presale_start and self.presale_end and self.presale_start > self.presale_end:
@@ -229,17 +238,17 @@ class EventMixin:
else: else:
return self.presale_end return self.presale_end
@property def presale_has_ended_by_time(self, now_dt: datetime=None):
def presale_has_ended(self):
""" """
Is true, when ``presale_end`` is set and in the past. Is true, when ``presale_end`` is set and in the past.
""" """
now_dt = now_dt or now()
if self.effective_presale_end: if self.effective_presale_end:
return now() > self.effective_presale_end return now_dt > self.effective_presale_end
elif self.date_to: elif self.date_to:
return now() > self.date_to return now_dt > self.date_to
else: else:
return now().astimezone(self.timezone).date() > self.date_from.astimezone(self.timezone).date() return now_dt.astimezone(self.timezone).date() > self.date_from.astimezone(self.timezone).date()
@property @property
def effective_presale_start(self): def effective_presale_start(self):
@@ -253,15 +262,15 @@ class EventMixin:
else: else:
return self.presale_start return self.presale_start
@property def presale_is_running_by_time(self, now_dt: datetime=None):
def presale_is_running(self):
""" """
Is true, when ``presale_end`` is not set or in the future and ``presale_start`` is not Is true, when ``presale_end`` is not set or in the future and ``presale_start`` is not
set or in the past. set or in the past.
""" """
if self.effective_presale_start and now() < self.effective_presale_start: now_dt = now_dt or now()
if self.effective_presale_start and now_dt < self.effective_presale_start:
return False return False
return not self.presale_has_ended return not self.presale_has_ended_by_time(now_dt)
@property @property
def event_microdata(self): def event_microdata(self):
@@ -683,12 +692,12 @@ class Event(EventMixin, LoggedModel):
return qs_annotated return qs_annotated
@property def presale_has_ended_by_time(self, now_dt: datetime = None):
def presale_has_ended(self): now_dt = now_dt or now()
if self.has_subevents: if self.has_subevents:
return self.presale_end and now() > self.presale_end return self.presale_end and now_dt > self.presale_end
else: else:
return super().presale_has_ended return super().presale_has_ended_by_time(now_dt)
def delete_all_orders(self, really=False): def delete_all_orders(self, really=False):
from .checkin import Checkin from .checkin import Checkin

View File

@@ -545,6 +545,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
) )
if getattr(self.request, 'customer', None) else None if getattr(self.request, 'customer', None) else None
), ),
now_dt=self.request.now_dt,
) )
item_cache[ckey] = items item_cache[ckey] = items
else: else:

View File

@@ -59,6 +59,7 @@ class WaitingListForm(forms.ModelForm):
) )
if customer else None if customer else None
), ),
now_dt=request.now_dt,
) )
for i in items: for i in items:
if not i.allow_waitinglist: if not i.allow_waitinglist:

View File

@@ -32,8 +32,11 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License. # License for the specific language governing permissions and limitations under the License.
from dateutil.parser import parse
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.urls import resolve from django.urls import resolve
from django.utils.timezone import now
from django_scopes import scope from django_scopes import scope
from pretix.base.channels import WebshopSalesChannel from pretix.base.channels import WebshopSalesChannel
@@ -79,3 +82,27 @@ class EventMiddleware:
response = response.render() response = response.render()
return response return response
class TimeMachineMiddleware:
def __init__(self, get_response=None):
self.get_response = get_response
super().__init__()
def __call__(self, request):
if hasattr(request, 'event') and hasattr(request, '_namespace') and request._namespace == 'presale' and \
'time_machine' in request.COOKIES and \
request.user.has_event_permission(request.organizer, request.event, 'can_change_event_settings', request):
print("setting now_dt from cookie")
request.now_dt = parse(request.COOKIES['time_machine'])
request.now_dt_is_fake = True
else:
print("setting now_dt to now",
"hasevent?",hasattr(request, 'event') ,
"namespace?",hasattr(request, '_namespace') and request._namespace,
"cookies?",request.COOKIES)
request.now_dt = now()
return self.get_response(request)

View File

@@ -33,6 +33,17 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if request.now_dt_is_fake %}
<div class="offline-banner">
<div class="container">
<span class="fa fa-user-secret" aria-hidden="true"></span>
{% trans "You are currently using the time machine. The ticket shop is rendered as if it were" %} {{ request.now_dt }}
<a href="#">
{% trans "Go back to current time" %}
</a>
</div>
</div>
{% endif %}
<div class="container page-header-links {% if event.settings.theme_color_background|upper != "#FFFFFF" or event_logo_image_large %}page-header-links-outside{% endif %}"> <div class="container page-header-links {% if event.settings.theme_color_background|upper != "#FFFFFF" or event_logo_image_large %}page-header-links-outside{% endif %}">
{% if event.settings.locales|length > 1 or request.organizer.settings.customer_accounts %} {% if event.settings.locales|length > 1 or request.organizer.settings.customer_accounts %}
{% if event.settings.theme_color_background|upper != "#FFFFFF" or event_logo_image_large %} {% if event.settings.theme_color_background|upper != "#FFFFFF" or event_logo_image_large %}

View File

@@ -569,6 +569,7 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, CartMixin, TemplateView
testmode=self.request.event.testmode testmode=self.request.event.testmode
) if getattr(self.request, 'customer', None) else None ) if getattr(self.request, 'customer', None) else None
), ),
now_dt=self.request.now_dt,
) )
# Calculate how many options the user still has. If there is only one option, we can # Calculate how many options the user still has. If there is only one option, we can

View File

@@ -55,7 +55,7 @@ class CheckoutView(View):
messages.error(request, _("Your cart is empty")) messages.error(request, _("Your cart is empty"))
return self.redirect(self.get_index_url(self.request)) return self.redirect(self.get_index_url(self.request))
if not request.event.presale_is_running: if not request.event.presale_is_running_by_time(request.now_dt):
messages.error(request, _("The booking period for this event is over or has not yet started.")) messages.error(request, _("The booking period for this event is over or has not yet started."))
return self.redirect(self.get_index_url(self.request)) return self.redirect(self.get_index_url(self.request))

View File

@@ -64,7 +64,7 @@ from pretix.base.channels import get_all_sales_channels
from pretix.base.models import ( from pretix.base.models import (
ItemVariation, Quota, SeatCategoryMapping, Voucher, ItemVariation, Quota, SeatCategoryMapping, Voucher,
) )
from pretix.base.models.event import Event, SubEvent from pretix.base.models.event import Event, SubEvent, annotate_with_time_based_properties
from pretix.base.models.items import ( from pretix.base.models.items import (
ItemAddOn, ItemBundle, SubEventItem, SubEventItemVariation, ItemAddOn, ItemBundle, SubEventItem, SubEventItemVariation,
) )
@@ -105,7 +105,8 @@ def item_group_by_category(items):
def get_grouped_items(event, subevent=None, voucher=None, channel='web', require_seat=0, base_qs=None, allow_addons=False, def get_grouped_items(event, subevent=None, voucher=None, channel='web', require_seat=0, base_qs=None, allow_addons=False,
quota_cache=None, filter_items=None, filter_categories=None, memberships=None, quota_cache=None, filter_items=None, filter_categories=None, memberships=None,
ignore_hide_sold_out_for_item_ids=None): ignore_hide_sold_out_for_item_ids=None, now_dt: datetime=None):
now_dt = now_dt or now()
base_qs_set = base_qs is not None base_qs_set = base_qs is not None
base_qs = base_qs if base_qs is not None else event.items base_qs = base_qs if base_qs is not None else event.items
@@ -119,8 +120,8 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
requires_seat = Value(0, output_field=IntegerField()) requires_seat = Value(0, output_field=IntegerField())
variation_q = ( variation_q = (
Q(Q(available_from__isnull=True) | Q(available_from__lte=now()) | Q(available_from_mode='info')) & Q(Q(available_from__isnull=True) | Q(available_from__lte=now_dt) | Q(available_from_mode='info')) &
Q(Q(available_until__isnull=True) | Q(available_until__gte=now()) | Q(available_until_mode='info')) Q(Q(available_until__isnull=True) | Q(available_until__gte=now_dt) | Q(available_until_mode='info'))
) )
if not voucher or not voucher.show_hidden_items: if not voucher or not voucher.show_hidden_items:
variation_q &= Q(hide_without_voucher=False) variation_q &= Q(hide_without_voucher=False)
@@ -137,8 +138,8 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
subevent_disabled=Exists( subevent_disabled=Exists(
SubEventItemVariation.objects.filter( SubEventItemVariation.objects.filter(
Q(disabled=True) Q(disabled=True)
| (Exact(OuterRef('available_from_mode'), 'hide') & Q(available_from__gt=now())) | (Exact(OuterRef('available_from_mode'), 'hide') & Q(available_from__gt=now_dt))
| (Exact(OuterRef('available_until_mode'), 'hide') & Q(available_until__lt=now())), | (Exact(OuterRef('available_until_mode'), 'hide') & Q(available_until__lt=now_dt)),
variation_id=OuterRef('pk'), variation_id=OuterRef('pk'),
subevent=subevent, subevent=subevent,
) )
@@ -209,8 +210,8 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
subevent_disabled=Exists( subevent_disabled=Exists(
SubEventItem.objects.filter( SubEventItem.objects.filter(
Q(disabled=True) Q(disabled=True)
| (Exact(OuterRef('available_from_mode'), 'hide') & Q(available_from__gt=now())) | (Exact(OuterRef('available_from_mode'), 'hide') & Q(available_from__gt=now_dt))
| (Exact(OuterRef('available_until_mode'), 'hide') & Q(available_until__lt=now())), | (Exact(OuterRef('available_until_mode'), 'hide') & Q(available_until__lt=now_dt)),
item_id=OuterRef('pk'), item_id=OuterRef('pk'),
subevent=subevent, subevent=subevent,
) )
@@ -306,7 +307,7 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
item._remove = True item._remove = True
continue continue
item.current_unavailability_reason = item.unavailability_reason(has_voucher=voucher, subevent=subevent) item.current_unavailability_reason = item.unavailability_reason(now_dt=now_dt, has_voucher=voucher, subevent=subevent)
item.description = str(item.description) item.description = str(item.description)
for recv, resp in item_description.send(sender=event, item=item, variation=None, subevent=subevent): for recv, resp in item_description.send(sender=event, item=item, variation=None, subevent=subevent):
@@ -422,7 +423,7 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
if not display_add_to_cart: if not display_add_to_cart:
display_add_to_cart = not item.requires_seat and var.order_max > 0 display_add_to_cart = not item.requires_seat and var.order_max > 0
var.current_unavailability_reason = var.unavailability_reason(has_voucher=voucher, subevent=subevent) var.current_unavailability_reason = var.unavailability_reason(now_dt=now_dt, has_voucher=voucher, subevent=subevent)
item.original_price = ( item.original_price = (
item.tax(item.original_price, currency=event.currency, include_bundled=True, item.tax(item.original_price, currency=event.currency, include_bundled=True,
@@ -535,6 +536,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
context['ev'] = self.subevent or self.request.event context['ev'] = self.subevent or self.request.event
context['subevent'] = self.subevent context['subevent'] = self.subevent
annotate_with_time_based_properties([self.request.event, self.subevent], self.request.now_dt)
# Show voucher option if an event is selected and vouchers exist # Show voucher option if an event is selected and vouchers exist
vouchers_exist = self.request.event.cache.get('vouchers_exist') vouchers_exist = self.request.event.cache.get('vouchers_exist')
@@ -543,10 +545,10 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
self.request.event.cache.set('vouchers_exist', vouchers_exist) self.request.event.cache.set('vouchers_exist', vouchers_exist)
context['show_vouchers'] = context['vouchers_exist'] = vouchers_exist and ( context['show_vouchers'] = context['vouchers_exist'] = vouchers_exist and (
(self.request.event.has_subevents and not self.subevent) or (self.request.event.has_subevents and not self.subevent) or
context['ev'].presale_is_running context['ev'].presale_is_running_by_time(self.request.now_dt)
) )
context['allow_waitinglist'] = self.request.event.settings.waiting_list_enabled and context['ev'].presale_is_running context['allow_waitinglist'] = self.request.event.settings.waiting_list_enabled and context['ev'].presale_is_running_by_time(self.request.now_dt)
if not self.request.event.has_subevents or self.subevent: if not self.request.event.has_subevents or self.subevent:
# Fetch all items # Fetch all items
@@ -562,6 +564,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
testmode=self.request.event.testmode testmode=self.request.event.testmode
) if getattr(self.request, 'customer', None) else None ) if getattr(self.request, 'customer', None) else None
), ),
now_dt=self.request.now_dt
) )
context['waitinglist_seated'] = False context['waitinglist_seated'] = False
@@ -612,7 +615,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
context['show_cart'] = ( context['show_cart'] = (
context['cart']['positions'] and ( context['cart']['positions'] and (
self.request.event.has_subevents or self.request.event.presale_is_running self.request.event.has_subevents or self.request.event.presale_is_running_by_time(self.request.now_dt)
) )
) )
if self.request.event.settings.redirect_to_checkout_directly: if self.request.event.settings.redirect_to_checkout_directly:
@@ -679,6 +682,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
limit_before, after, ebd, set(), self.request.event, limit_before, after, ebd, set(), self.request.event,
self.kwargs.get('cart_namespace'), self.kwargs.get('cart_namespace'),
voucher, voucher,
now_dt=self.request.now_dt,
) )
# Hide names of subevents in event series where it is always the same. No need to show the name of the museum thousands of times # Hide names of subevents in event series where it is always the same. No need to show the name of the museum thousands of times
@@ -738,6 +742,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
limit_before, after, ebd, set(), self.request.event, limit_before, after, ebd, set(), self.request.event,
self.kwargs.get('cart_namespace'), self.kwargs.get('cart_namespace'),
voucher, voucher,
now_dt=self.request.now_dt,
) )
# Hide names of subevents in event series where it is always the same. No need to show the name of the museum thousands of times # Hide names of subevents in event series where it is always the same. No need to show the name of the museum thousands of times
@@ -776,7 +781,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
future_only=self.request.event.settings.event_calendar_future_only future_only=self.request.event.settings.event_calendar_future_only
) )
else: else:
context['subevent_list'] = self.request.event.subevents_sorted( context['subevent_list'] = annotate_with_time_based_properties(self.request.event.subevents_sorted(
filter_qs_by_attr( filter_qs_by_attr(
self.request.event.subevents_annotated( self.request.event.subevents_annotated(
self.request.sales_channel.identifier, self.request.sales_channel.identifier,
@@ -784,7 +789,8 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
).using(settings.DATABASE_REPLICA), ).using(settings.DATABASE_REPLICA),
self.request self.request
) )
) ), self.request.now_dt)
if self.request.event.settings.event_list_available_only and not voucher: if self.request.event.settings.event_list_available_only and not voucher:
context['subevent_list'] = [ context['subevent_list'] = [
se for se in context['subevent_list'] se for se in context['subevent_list']

View File

@@ -1311,7 +1311,8 @@ class OrderChangeMixin:
) )
if self.order.customer else None if self.order.customer else None
), ),
ignore_hide_sold_out_for_item_ids={k[0] for k in current_addon_products.keys()} ignore_hide_sold_out_for_item_ids={k[0] for k in current_addon_products.keys()},
now_dt=self.request.now_dt,
) )
item_cache[ckey] = items item_cache[ckey] = items
else: else:

View File

@@ -63,6 +63,7 @@ from pretix.base.i18n import language
from pretix.base.models import ( from pretix.base.models import (
Event, EventMetaValue, Organizer, Quota, SubEvent, SubEventMetaValue, Event, EventMetaValue, Organizer, Quota, SubEvent, SubEventMetaValue,
) )
from pretix.base.models.event import annotate_with_time_based_properties
from pretix.base.services.quotas import QuotaAvailability from pretix.base.services.quotas import QuotaAvailability
from pretix.helpers.compat import date_fromisocalendar from pretix.helpers.compat import date_fromisocalendar
from pretix.helpers.daterange import daterange from pretix.helpers.daterange import daterange
@@ -549,14 +550,16 @@ def add_events_for_days(request, baseqs, before, after, ebd, timezones):
}) })
def add_subevents_for_days(qs, before, after, ebd, timezones, event=None, cart_namespace=None, voucher=None): def add_subevents_for_days(qs, before, after, ebd, timezones, event=None, cart_namespace=None, voucher=None, now_dt=None):
print("add_subevents_for_days", now_dt)
now_dt = now_dt or now()
qs = qs.filter(active=True, is_public=True).filter( qs = qs.filter(active=True, is_public=True).filter(
Q(Q(date_to__gte=before) & Q(date_from__lte=after)) | Q(Q(date_to__gte=before) & Q(date_from__lte=after)) |
Q(Q(date_to__isnull=True) & Q(date_from__gte=before) & Q(date_from__lte=after)) Q(Q(date_to__isnull=True) & Q(date_from__gte=before) & Q(date_from__lte=after))
).order_by( ).order_by(
'date_from' 'date_from'
) )
qs = annotate_with_time_based_properties(qs, now_dt)
quotas_to_compute = [] quotas_to_compute = []
for se in qs: for se in qs:
if se.presale_is_running: if se.presale_is_running:

View File

@@ -256,6 +256,7 @@ class WidgetAPIProductList(EventListMixin, View):
testmode=self.request.event.testmode testmode=self.request.event.testmode
) if getattr(self.request, 'customer', None) else None ) if getattr(self.request, 'customer', None) else None
), ),
now_dt=self.request.now_dt,
) )
grps = [] grps = []
@@ -409,11 +410,11 @@ class WidgetAPIProductList(EventListMixin, View):
availability['color'] = 'none' availability['color'] = 'none'
availability['text'] = gettext('More info') availability['text'] = gettext('More info')
availability['reason'] = 'unknown' availability['reason'] = 'unknown'
elif ev.presale_is_running: elif ev.presale_is_running_by_time(self.request.now_dt):
availability['color'] = 'green' availability['color'] = 'green'
availability['text'] = gettext('Book now') availability['text'] = gettext('Book now')
availability['reason'] = 'ok' availability['reason'] = 'ok'
elif ev.presale_has_ended: elif ev.presale_has_ended_by_time(self.request.now_dt):
availability['color'] = 'red' availability['color'] = 'red'
availability['text'] = gettext('Sale over') availability['text'] = gettext('Sale over')
availability['reason'] = 'over' availability['reason'] = 'over'

View File

@@ -443,6 +443,7 @@ MIDDLEWARE = [
'pretix.base.middleware.LocaleMiddleware', 'pretix.base.middleware.LocaleMiddleware',
'pretix.base.middleware.SecurityMiddleware', 'pretix.base.middleware.SecurityMiddleware',
'pretix.presale.middleware.EventMiddleware', 'pretix.presale.middleware.EventMiddleware',
'pretix.presale.middleware.TimeMachineMiddleware',
'pretix.api.middleware.ApiScopeMiddleware', 'pretix.api.middleware.ApiScopeMiddleware',
] ]