Compare commits

...

3 Commits

Author SHA1 Message Date
Raphael Michel
9c59206949 Review notes 2025-09-11 19:19:12 +02:00
Raphael Michel
b8f7aff5e4 Add to docs 2025-09-09 17:00:50 +02:00
Raphael Michel
6e4f32153f Allow plugins to filter subevents in the public calendar 2025-09-09 16:39:00 +02:00
5 changed files with 210 additions and 84 deletions

View File

@@ -37,7 +37,7 @@ Frontend
--------
.. automodule:: pretix.presale.signals
:members: html_head, html_footer, footer_link, global_footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, contact_form_fields_overrides, question_form_fields_overrides, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header, seatingframe_html_head
:members: html_head, html_footer, footer_link, global_footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, contact_form_fields_overrides, question_form_fields_overrides, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header, seatingframe_html_head, filter_subevents
.. automodule:: pretix.presale.signals

View File

@@ -415,3 +415,19 @@ consent state. Receivers should return a list of ``pretix.presale.cookies.Cookie
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
filter_subevents = GlobalSignal()
"""
Arguments: ``subevents``, ``sales_channel``
This signal allows you to filter which subevents are publicly available. Receivers are passed a
list of subevents that are about to be shown to the user and are expected to return a list of the
same format, with all subevents removed that should not be available for sale.
``sales_channels`` is a ``SalesChannel`` instance.
This is not an event-plugin signal as this will also be called on the organizer level when showing
a list of subevents across events. Expect that the subevents in the input are mixed from different
events. However, receivers will only receive subevents of events that the plugin is active for and
can only filter out these.
"""

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']: