diff --git a/src/pretix/base/templatetags/cache_large.py b/src/pretix/base/templatetags/cache_large.py new file mode 100644 index 0000000000..16099bcd1b --- /dev/null +++ b/src/pretix/base/templatetags/cache_large.py @@ -0,0 +1,35 @@ +from django.conf import settings +from django.template import Library, Node, TemplateSyntaxError, Variable +from django.templatetags.cache import CacheNode + +register = Library() + + +class DummyNode(Node): + def __init__(self, nodelist, *args): + self.nodelist = nodelist + + def render(self, context): + value = self.nodelist.render(context) + return value + + +@register.tag('cache_large') +def do_cache(parser, token): + nodelist = parser.parse(('endcache_large',)) + parser.delete_first_token() + tokens = token.split_contents() + if len(tokens) < 3: + raise TemplateSyntaxError("'%r' tag requires at least 2 arguments." % tokens[0]) + + if not settings.CACHE_LARGE_VALUES_ALLOWED: + return DummyNode( + nodelist, + ) + + return CacheNode( + nodelist, parser.compile_filter(tokens[1]), + tokens[2], # fragment_name can't be a variable. + [parser.compile_filter(t) for t in tokens[3:]], + Variable(repr(settings.CACHE_LARGE_VALUES_ALIAS)), + ) diff --git a/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar.html b/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar.html index 37259a5bdb..36f59bdfda 100644 --- a/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar.html +++ b/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar.html @@ -9,21 +9,21 @@ {% endfor %}
-{% include "pretixpresale/fragment_calendar.html" with show_avail=event.settings.event_list_availability %} +{% include "pretixpresale/fragment_calendar.html" with show_avail=event.settings.event_list_availability weeks=subevent_list.weeks %} diff --git a/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar_week.html b/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar_week.html index 5e30d36b07..ee4cf16514 100644 --- a/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar_week.html +++ b/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar_week.html @@ -9,21 +9,21 @@ {% endfor %}
-{% include "pretixpresale/fragment_week_calendar.html" with show_avail=event.settings.event_list_availability %} +{% include "pretixpresale/fragment_week_calendar.html" with show_avail=event.settings.event_list_availability days=subevent_list.days %}
- - {{ before|date:week_format }} + {{ subevent_list.before|date:subevent_list.week_format }} - - {{ after|date:week_format }} + {{ subevent_list.after|date:subevent_list.week_format }}
diff --git a/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_list.html b/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_list.html index 7cfa42e89c..6c25419a14 100644 --- a/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_list.html +++ b/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_list.html @@ -1,6 +1,6 @@ {% load i18n %} {% load eventurl %} -{% for subev in subevent_list %} +{% for subev in subevent_list.subevent_list %}
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