Allow plugins to filter subevents in the public calendar (#5457)

* Allow plugins to filter subevents in the public calendar

* Add to docs

* Review notes
This commit is contained in:
Raphael Michel
2025-09-11 19:40:10 +02:00
committed by GitHub
parent b3974067a5
commit ed9250c522
5 changed files with 210 additions and 84 deletions

View File

@@ -85,7 +85,8 @@ from pretix.presale.ical import get_public_ical
from pretix.presale.signals import item_description, seatingframe_html_head
from pretix.presale.views.organizer import (
EventListMixin, add_subevents_for_days, days_for_template,
filter_qs_by_attr, has_before_after, weeks_for_template,
filter_qs_by_attr, filter_subevents_with_plugins, has_before_after,
weeks_for_template,
)
from . import (
@@ -546,6 +547,12 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
self.subevent = request.event.subevents.using(settings.DATABASE_REPLICA).filter(pk=kwargs['subevent'], active=True).first()
if not self.subevent:
raise Http404()
# Prevent direct access to subevents that are hidden by a plugin
subevents = filter_subevents_with_plugins([self.subevent], self.request.sales_channel)
if self.subevent not in subevents:
raise Http404()
return super().get(request, *args, **kwargs)
else:
return super().get(request, *args, **kwargs)
@@ -703,9 +710,14 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
).using(settings.DATABASE_REPLICA),
self.request
),
limit_before, after, ebd, set(), self.request.event,
self.kwargs.get('cart_namespace'),
voucher,
before=limit_before,
after=after,
ebd=ebd,
timezones=set(),
event=self.request.event,
cart_namespace=self.kwargs.get('cart_namespace'),
voucher=voucher,
sales_channel=self.request.sales_channel,
)
# 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
@@ -762,9 +774,14 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
).using(settings.DATABASE_REPLICA),
self.request
),
limit_before, after, ebd, set(), self.request.event,
self.kwargs.get('cart_namespace'),
voucher,
before=limit_before,
after=after,
ebd=ebd,
timezones=set(),
event=self.request.event,
cart_namespace=self.kwargs.get('cart_namespace'),
voucher=voucher,
sales_channel=self.request.sales_channel,
)
# 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
@@ -803,7 +820,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
future_only=self.request.event.settings.event_calendar_future_only
)
else:
context['subevent_list'] = self.request.event.subevents_sorted(
subevents = self.request.event.subevents_sorted(
filter_qs_by_attr(
self.request.event.subevents_annotated(
self.request.sales_channel,
@@ -812,12 +829,14 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
self.request
)
)
subevents = filter_subevents_with_plugins(list(subevents), self.request.sales_channel)
context['subevent_list'] = subevents
if self.request.event.settings.event_list_available_only and not voucher:
context['subevent_list'] = [
se for se in context['subevent_list']
se for se in subevents
if not se.presale_has_ended and (se.best_availability_state is None or se.best_availability_state >= Quota.AVAILABILITY_RESERVED)
]
context['visible_events'] = len(context['subevent_list']) > 0
context['visible_events'] = len(subevents) > 0
return context

View File

@@ -49,6 +49,7 @@ from django.db.models import (
Case, Exists, F, Max, Min, OuterRef, Prefetch, Q, Value, When,
)
from django.db.models.functions import Coalesce, Greatest
from django.dispatch.dispatcher import NO_RECEIVERS
from django.http import Http404, HttpResponse, QueryDict
from django.templatetags.static import static
from django.utils.decorators import method_decorator
@@ -76,6 +77,7 @@ from pretix.helpers.thumb import get_thumbnail
from pretix.multidomain.urlreverse import build_absolute_uri, eventreverse
from pretix.presale.forms.organizer import EventListFilterForm
from pretix.presale.ical import get_public_ical
from pretix.presale.signals import filter_subevents
from pretix.presale.views import OrganizerViewMixin
@@ -561,16 +563,56 @@ 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 filter_subevents_with_plugins(subevents, sales_channel=None):
# Special-case of GlobalSignal.send_chained() that only sends subevents that have the plugin enabled
# and then mixes the results back together.
from pretix.base.signals import (
_populate_app_cache, app_cache, get_defining_app, is_app_active,
)
if not filter_subevents.receivers or filter_subevents.sender_receivers_cache.get(None) is NO_RECEIVERS:
return subevents
if not app_cache:
_populate_app_cache()
for receiver in filter_subevents._live_receivers(None):
app = get_defining_app(receiver)
event_state = {}
def app_active(event):
if event.pk not in event_state:
event_state[event.pk] = is_app_active(event, app, allow_legacy_plugins=True)
return event_state[event.pk]
subevents_passed_to_receiver = [
s for s in subevents if app_active(s.event)
]
response = receiver(
signal=filter_subevents,
sender=None,
subevents=subevents_passed_to_receiver,
sales_channel=sales_channel,
)
subevents = [
s for s in subevents if s in response or s not in subevents_passed_to_receiver
]
return subevents
def add_subevents_for_days(qs, before, after, ebd, timezones, sales_channel, event=None, cart_namespace=None,
voucher=None):
qs = qs.filter(active=True, is_public=True).filter(
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))
).order_by(
'date_from'
)
subevents = filter_subevents_with_plugins(list(qs), sales_channel)
quotas_to_compute = []
for se in qs:
for se in subevents:
if se.presale_is_running:
quotas_to_compute += se.active_quotas
for q in se.active_quotas:
@@ -588,7 +630,7 @@ def add_subevents_for_days(qs, before, after, ebd, timezones, event=None, cart_n
qa.compute(allow_cache=True)
qcache.update(qa.results)
for se in qs:
for se in subevents:
if qcache:
se._quota_cache = qcache
if event is not None: # save database lookup later
@@ -763,24 +805,31 @@ class CalendarView(OrganizerViewMixin, EventListMixin, TemplateView):
).filter(
Q(all_sales_channels=True) | Q(limit_sales_channels=self.request.sales_channel),
), before, after, ebd, timezones)
add_subevents_for_days(filter_qs_by_attr(SubEvent.annotated(SubEvent.objects.filter(
Q(event__all_sales_channels=True) |
Q(event__limit_sales_channels=self.request.sales_channel),
event__organizer=self.request.organizer,
event__is_public=True,
event__live=True,
).prefetch_related(
Prefetch(
'event',
queryset=Event.objects.prefetch_related(
'_settings_objects',
Prefetch(
'organizer',
queryset=Organizer.objects.prefetch_related('_settings_objects')
add_subevents_for_days(
filter_qs_by_attr(SubEvent.annotated(SubEvent.objects.filter(
Q(event__all_sales_channels=True) |
Q(event__limit_sales_channels=self.request.sales_channel),
event__organizer=self.request.organizer,
event__is_public=True,
event__live=True,
).prefetch_related(
Prefetch(
'event',
queryset=Event.objects.prefetch_related(
'_settings_objects',
Prefetch(
'organizer',
queryset=Organizer.objects.prefetch_related('_settings_objects')
)
)
)
)
), self.request.sales_channel), self.request).using(settings.DATABASE_REPLICA), before, after, ebd, timezones)
), self.request.sales_channel), self.request).using(settings.DATABASE_REPLICA),
before=before,
after=after,
ebd=ebd,
timezones=timezones,
sales_channel=self.request.sales_channel,
)
self._multiple_timezones = len(timezones) > 1
return ebd
@@ -860,24 +909,31 @@ class WeekCalendarView(OrganizerViewMixin, EventListMixin, TemplateView):
).filter(
Q(all_sales_channels=True) | Q(limit_sales_channels=self.request.sales_channel),
), before, after, ebd, timezones)
add_subevents_for_days(filter_qs_by_attr(SubEvent.annotated(SubEvent.objects.filter(
Q(event__all_sales_channels=True) |
Q(event__limit_sales_channels=self.request.sales_channel),
event__organizer=self.request.organizer,
event__is_public=True,
event__live=True,
).prefetch_related(
Prefetch(
'event',
queryset=Event.objects.prefetch_related(
'_settings_objects',
Prefetch(
'organizer',
queryset=Organizer.objects.prefetch_related('_settings_objects')
add_subevents_for_days(
filter_qs_by_attr(SubEvent.annotated(SubEvent.objects.filter(
Q(event__all_sales_channels=True) |
Q(event__limit_sales_channels=self.request.sales_channel),
event__organizer=self.request.organizer,
event__is_public=True,
event__live=True,
).prefetch_related(
Prefetch(
'event',
queryset=Event.objects.prefetch_related(
'_settings_objects',
Prefetch(
'organizer',
queryset=Organizer.objects.prefetch_related('_settings_objects')
)
)
)
)
), self.request.sales_channel), self.request).using(settings.DATABASE_REPLICA), before, after, ebd, timezones)
), self.request.sales_channel), self.request).using(settings.DATABASE_REPLICA),
before=before,
after=after,
ebd=ebd,
timezones=timezones,
sales_channel=self.request.sales_channel,
)
self._multiple_timezones = len(timezones) > 1
return ebd
@@ -1211,24 +1267,31 @@ class DayCalendarView(OrganizerViewMixin, EventListMixin, TemplateView):
).filter(
Q(all_sales_channels=True) | Q(limit_sales_channels=self.request.sales_channel),
), before, after, ebd, timezones)
add_subevents_for_days(filter_qs_by_attr(SubEvent.annotated(SubEvent.objects.filter(
Q(event__all_sales_channels=True) |
Q(event__limit_sales_channels=self.request.sales_channel),
event__organizer=self.request.organizer,
event__is_public=True,
event__live=True,
).prefetch_related(
Prefetch(
'event',
queryset=Event.objects.prefetch_related(
'_settings_objects',
Prefetch(
'organizer',
queryset=Organizer.objects.prefetch_related('_settings_objects')
add_subevents_for_days(
filter_qs_by_attr(SubEvent.annotated(SubEvent.objects.filter(
Q(event__all_sales_channels=True) |
Q(event__limit_sales_channels=self.request.sales_channel),
event__organizer=self.request.organizer,
event__is_public=True,
event__live=True,
).prefetch_related(
Prefetch(
'event',
queryset=Event.objects.prefetch_related(
'_settings_objects',
Prefetch(
'organizer',
queryset=Organizer.objects.prefetch_related('_settings_objects')
)
)
)
)
), self.request.sales_channel), self.request).using(settings.DATABASE_REPLICA), before, after, ebd, timezones)
), self.request.sales_channel), self.request).using(settings.DATABASE_REPLICA),
before=before,
after=after,
ebd=ebd,
timezones=timezones,
sales_channel=self.request.sales_channel,
)
self._multiple_timezones = len(timezones) > 1
return ebd

View File

@@ -73,7 +73,8 @@ from pretix.presale.views.event import (
)
from pretix.presale.views.organizer import (
EventListMixin, add_events_for_days, add_subevents_for_days,
days_for_template, filter_qs_by_attr, weeks_for_template,
days_for_template, filter_qs_by_attr, filter_subevents_with_plugins,
weeks_for_template,
)
logger = logging.getLogger(__name__)
@@ -403,6 +404,14 @@ class WidgetAPIProductList(EventListMixin, View):
return self.response({
'error': gettext('The selected date does not exist in this event series.')
})
# Prevent direct access to subevents that are hidden by a plugin
subevents = filter_subevents_with_plugins([self.subevent], request.sales_channel)
if self.subevent not in subevents:
return self.response({
'error': gettext('The selected date is not available.')
})
else:
return self._get_event_list(request, **kwargs)
else:
@@ -568,8 +577,9 @@ class WidgetAPIProductList(EventListMixin, View):
Q(event__limit_sales_channels=self.request.sales_channel),
), self.request
),
limit_before, after, ebd, set(), self.request.event,
kwargs.get('cart_namespace')
before=limit_before, after=after, ebd=ebd, timezones=set(), event=self.request.event,
cart_namespace=kwargs.get('cart_namespace'),
sales_channel=self.request.sales_channel,
)
else:
timezones = set()
@@ -580,17 +590,27 @@ class WidgetAPIProductList(EventListMixin, View):
Q(all_sales_channels=True) | Q(limit_sales_channels=self.request.sales_channel),
), self.request
),
limit_before, after, ebd, timezones
before=limit_before,
after=after,
ebd=ebd,
timezones=timezones,
)
add_subevents_for_days(
filter_qs_by_attr(SubEvent.annotated(SubEvent.objects.filter(
Q(event__all_sales_channels=True) |
Q(event__limit_sales_channels__identifier=self.request.sales_channel.identifier),
event__organizer=self.request.organizer,
event__is_public=True,
event__live=True,
).prefetch_related(
'event___settings_objects', 'event__organizer___settings_objects'
), self.request.sales_channel), self.request),
before=limit_before,
after=after,
ebd=ebd,
timezones=timezones,
sales_channel=self.request.sales_channel,
)
add_subevents_for_days(filter_qs_by_attr(SubEvent.annotated(SubEvent.objects.filter(
Q(event__all_sales_channels=True) |
Q(event__limit_sales_channels__identifier=self.request.sales_channel.identifier),
event__organizer=self.request.organizer,
event__is_public=True,
event__live=True,
).prefetch_related(
'event___settings_objects', 'event__organizer___settings_objects'
), self.request.sales_channel), self.request), limit_before, after, ebd, timezones)
data['weeks'] = weeks_for_template(ebd, self.year, self.month)
for w in data['weeks']:
@@ -624,8 +644,9 @@ class WidgetAPIProductList(EventListMixin, View):
if hasattr(self.request, 'event'):
add_subevents_for_days(
filter_qs_by_attr(self.request.event.subevents_annotated(self.request.sales_channel), self.request),
limit_before, after, ebd, set(), self.request.event,
kwargs.get('cart_namespace')
before=limit_before, after=after, ebd=ebd, timezones=set(), event=self.request.event,
cart_namespace=kwargs.get('cart_namespace'),
sales_channel=self.request.sales_channel,
)
else:
timezones = set()
@@ -634,13 +655,20 @@ class WidgetAPIProductList(EventListMixin, View):
filter_qs_by_attr(Event.annotated(self.request.organizer.events, self.request.sales_channel), self.request),
limit_before, after, ebd, timezones
)
add_subevents_for_days(filter_qs_by_attr(SubEvent.annotated(SubEvent.objects.filter(
event__organizer=self.request.organizer,
event__is_public=True,
event__live=True,
).prefetch_related(
'event___settings_objects', 'event__organizer___settings_objects'
), self.request.sales_channel), self.request), limit_before, after, ebd, timezones)
add_subevents_for_days(
filter_qs_by_attr(SubEvent.annotated(SubEvent.objects.filter(
event__organizer=self.request.organizer,
event__is_public=True,
event__live=True,
).prefetch_related(
'event___settings_objects', 'event__organizer___settings_objects'
), self.request.sales_channel), self.request),
before=limit_before,
after=after,
ebd=ebd,
timezones=timezones,
sales_channel=self.request.sales_channel,
)
data['days'] = days_for_template(ebd, week)
for d in data['days']: