diff --git a/src/pretix/presale/templates/pretixpresale/event/index.html b/src/pretix/presale/templates/pretixpresale/event/index.html
index 59d684460e..e0dc8be3cf 100644
--- a/src/pretix/presale/templates/pretixpresale/event/index.html
+++ b/src/pretix/presale/templates/pretixpresale/event/index.html
@@ -2,6 +2,7 @@
{% load i18n %}
{% load l10n %}
{% load eventurl %}
+{% load cache_large %}
{% load money %}
{% load thumb %}
{% load eventsignal %}
@@ -73,13 +74,15 @@
- {% if list_type == "calendar" %}
- {% include "pretixpresale/event/fragment_subevent_calendar.html" %}
- {% elif list_type == "week" %}
- {% include "pretixpresale/event/fragment_subevent_calendar_week.html" %}
- {% else %}
- {% include "pretixpresale/event/fragment_subevent_list.html" %}
- {% endif %}
+ {% cache_large 15 subevent_lits subevent_list_cache_key %}
+ {% if subevent_list.list_type == "calendar" %}
+ {% include "pretixpresale/event/fragment_subevent_calendar.html" %}
+ {% elif subevent_list.list_type == "week" %}
+ {% include "pretixpresale/event/fragment_subevent_calendar_week.html" %}
+ {% else %}
+ {% include "pretixpresale/event/fragment_subevent_list.html" %}
+ {% endif %}
+ {% endcache_large %}
diff --git a/src/pretix/presale/views/event.py b/src/pretix/presale/views/event.py
index 5a7f3ef2b7..14efb87788 100644
--- a/src/pretix/presale/views/event.py
+++ b/src/pretix/presale/views/event.py
@@ -33,6 +33,7 @@
# License for the specific language governing permissions and limitations under the License.
import calendar
+import hashlib
import sys
from collections import defaultdict
from datetime import date, datetime, timedelta
@@ -50,6 +51,7 @@ from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.decorators import method_decorator
from django.utils.formats import get_format
+from django.utils.functional import SimpleLazyObject
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django.views import View
@@ -448,7 +450,8 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
context['frontpage_text'] = str(self.request.event.settings.frontpage_text)
if self.request.event.has_subevents:
- context.update(self._subevent_list_context())
+ context['subevent_list'] = SimpleLazyObject(self._subevent_list_context)
+ context['subevent_list_cache_key'] = self._subevent_list_cachekey()
context['show_cart'] = (
context['cart']['positions'] and (
@@ -465,6 +468,17 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
return context
+ def _subevent_list_cachekey(self):
+ cache_key_parts = [
+ self.request.host,
+ str(self.request.event.pk),
+ self.request.get_full_path(),
+ self.request.LANGUAGE_CODE,
+ self.request.sales_channel.identifier,
+ ]
+ cache_key = f'pretix.presale.views.event.EventIndex.subevent_list_context:{hashlib.md5(":".join(cache_key_parts).encode()).hexdigest()}'
+ return cache_key
+
def _subevent_list_context(self):
voucher = None
if self.request.GET.get('voucher'):
diff --git a/src/pretix/presale/views/organizer.py b/src/pretix/presale/views/organizer.py
index beda847a47..725ecf740a 100644
--- a/src/pretix/presale/views/organizer.py
+++ b/src/pretix/presale/views/organizer.py
@@ -33,6 +33,7 @@
# License for the specific language governing permissions and limitations under the License.
import calendar
+import hashlib
from collections import defaultdict
from datetime import date, datetime, time, timedelta
from urllib.parse import quote
@@ -40,6 +41,7 @@ from urllib.parse import quote
import isoweek
import pytz
from django.conf import settings
+from django.core.cache import caches
from django.db.models import Exists, Max, Min, OuterRef, Q
from django.db.models.functions import Coalesce, Greatest
from django.http import Http404, HttpResponse
@@ -306,6 +308,68 @@ class OrganizerIndex(OrganizerViewMixin, EventListMixin, ListView):
template_name = 'pretixpresale/organizers/index.html'
paginate_by = 30
+ def dispatch(self, request, *args, **kwargs):
+ # In stock pretix, nothing on this page is session-dependent except for the language and the customer login part,
+ # so we can cache pretty aggressively if the user is anonymous. Note that we deliberately implement the caching
+ # on the view layer, *after* all middlewares have been ran, so we have access to the computed locale, as well
+ # as the login status etc.
+ cache_allowed = (
+ settings.CACHE_LARGE_VALUES_ALLOWED and
+ not getattr(request, 'customer', None) and
+ not request.user.is_authenticated
+ )
+
+ if not cache_allowed:
+ return super().dispatch(request, *args, **kwargs)
+
+ cache_key_parts = [
+ request.method,
+ request.host,
+ str(request.organizer.pk),
+ request.get_full_path(),
+ request.LANGUAGE_CODE,
+ self.request.sales_channel.identifier,
+ ]
+ for c, v in request.COOKIES.items():
+ # If the cookie is not one we know, it might be set by a plugin and we need to include it in the
+ # cache key to be safe. A known example includes plugins that e.g. store cookie banner state.
+ if c not in (settings.SESSION_COOKIE_NAME, settings.LANGUAGE_COOKIE_NAME, settings.CSRF_COOKIE_NAME):
+ cache_key_parts.append(f'{c}={v}')
+ for c, v in request.session.items():
+ # If the session key is not one we know, it might be set by a plugin and we need to include it in the
+ # cache key to be safe. A known example would be the pretix-campaigns plugin setting the campaign ID.
+ if (
+ not c.startswith('_auth') and
+ not c.startswith('pretix_auth_') and
+ not c.startswith('customer_auth_') and
+ not c.startswith('current_cart_') and
+ not c.startswith('cart_') and
+ not c.startswith('payment_') and
+ c not in ('carts', 'payment', 'pinned_user_agent')
+ ):
+ cache_key_parts.append(f'{c}={repr(v)}')
+
+ cache_key = f'pretix.presale.views.organizer.OrganizerIndex:{hashlib.md5(":".join(cache_key_parts).encode()).hexdigest()}'
+ cache_timeout = 15
+ cache = caches[settings.CACHE_LARGE_VALUES_ALIAS]
+
+ response = cache.get(cache_key)
+ if response is not None:
+ return response
+
+ response = super().dispatch(request, *kwargs, **kwargs)
+ if response.status_code >= 400:
+ return response
+
+ if hasattr(response, 'render') and callable(response.render):
+ def _store_to_cache(r):
+ cache.set(cache_key, r, cache_timeout)
+
+ response.add_post_render_callback(_store_to_cache)
+ else:
+ cache.set(cache_key, response, cache_timeout)
+ return response
+
def get(self, request, *args, **kwargs):
style = request.GET.get("style", request.organizer.settings.event_list_type)
if style == "calendar":
diff --git a/src/pretix/settings.py b/src/pretix/settings.py
index 75f51cef87..d5253271af 100644
--- a/src/pretix/settings.py
+++ b/src/pretix/settings.py
@@ -233,6 +233,11 @@ CACHES = {
REAL_CACHE_USED = False
SESSION_ENGINE = None
+# pretix includes caching options for some special situations where full HTML responses are cached. This might be
+# stressful for some cache setups so it is enabled by default and currently can't be enabled through pretix.cfg
+CACHE_LARGE_VALUES_ALLOWED = False
+CACHE_LARGE_VALUES_ALIAS = 'default'
+
HAS_MEMCACHED = config.has_option('memcached', 'location')
if HAS_MEMCACHED:
REAL_CACHE_USED = True