mirror of
https://github.com/pretix/pretix.git
synced 2025-12-07 22:42:26 +00:00
Compare commits
3 Commits
import-tes
...
subevent-f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c59206949 | ||
|
|
b8f7aff5e4 | ||
|
|
6e4f32153f |
@@ -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
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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']:
|
||||
|
||||
Reference in New Issue
Block a user