Performance: Cache complete organizer index page, cache subevent list template fragment (#2125)

This commit is contained in:
Raphael Michel
2021-06-15 00:02:47 +02:00
parent 5801c8602e
commit 72388abd57
8 changed files with 152 additions and 31 deletions

View File

@@ -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)),
)

View File

@@ -9,21 +9,21 @@
{% endfor %}
<div class="row">
<div class="col-sm-4 hidden-xs text-left flip">
<a href="?{% url_replace request "year" before.year "month" before.month %}"
<a href="?{% url_replace request "year" subevent_list.before.year "month" subevent_list.before.month %}"
class="btn btn-default">
<span class="fa fa-arrow-left" aria-hidden="true"></span>
{{ before|date:"F Y" }}
{{ subevent_list.before|date:"F Y" }}
</a>
</div>
<div class="col-sm-4 col-xs-12 text-center">
<select name="month" class="form-control">
{% for m in months %}
<option value="{{ m|date:"m" }}" {% if m == date %}selected{% endif %}>{{ m|date:"F" }}</option>
{% for m in subevent_list.months %}
<option value="{{ m|date:"m" }}" {% if m == subevent_list.date.month %}selected{% endif %}>{{ m|date:"F" }}</option>
{% endfor %}
</select>
<select name="year" class="form-control">
{% for y in years %}
<option value="{{ y }}" {% if y == date.year %}selected{% endif %}>{{ y }}</option>
{% for y in subevent_list.years %}
<option value="{{ y }}" {% if y == subevent_list.date.year %}selected{% endif %}>{{ y }}</option>
{% endfor %}
</select>
<button type="submit" class="js-hidden btn btn-default">
@@ -31,11 +31,11 @@
</button>
</div>
<div class="col-sm-4 hidden-xs text-right flip">
<a href="?{% url_replace request "year" after.year "month" after.month %}" class="btn btn-default">
{{ after|date:"F Y" }}
<a href="?{% url_replace request "year" subevent_list.after.year "month" subevent_list.after.month %}" class="btn btn-default">
{{ subevent_list.after|date:"F Y" }}
<span class="fa fa-arrow-right" aria-hidden="true"></span>
</a>
</div>
</div>
</form>
{% 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 %}

View File

@@ -9,21 +9,21 @@
{% endfor %}
<div class="row">
<div class="col-sm-4 hidden-xs text-left flip">
<a href="?{% url_replace request "year" before.isocalendar.0 "week" before.isocalendar.1 %}"
<a href="?{% url_replace request "year" subevent_list.before.isocalendar.0 "week" subevent_list.before.isocalendar.1 %}"
class="btn btn-default">
<span class="fa fa-arrow-left" aria-hidden="true"></span>
{{ before|date:week_format }}
{{ before|date:subevent_list.week_format }}
</a>
</div>
<div class="col-sm-4 col-xs-12 text-center">
<select name="week" class="form-control select-calendar-week-short">
{% for w in weeks %}
<option value="{{ w.0.isocalendar.1 }}" {% if w.0.isocalendar.1 == date.isocalendar.1 %}selected{% endif %}>{% trans "W" %} {{ w.0.isocalendar.1 }} ({{ w.0|date:"SHORT_DATE_FORMAT" }} {{ w.1|date:"SHORT_DATE_FORMAT" }})</option>
{% for w in subevent_list.weeks %}
<option value="{{ w.0.isocalendar.1 }}" {% if w.0.isocalendar.1 == subevent_list.date.isocalendar.1 %}selected{% endif %}>{% trans "W" %} {{ w.0.isocalendar.1 }} ({{ w.0|date:"SHORT_DATE_FORMAT" }} {{ w.1|date:"SHORT_DATE_FORMAT" }})</option>
{% endfor %}
</select>
<select name="year" class="form-control">
{% for y in years %}
<option value="{{ y }}" {% if y == date.isocalendar.0 %}selected{% endif %}>{{ y }}</option>
{% for y in subevent_list.years %}
<option value="{{ y }}" {% if y == subevent_list.date.isocalendar.0 %}selected{% endif %}>{{ y }}</option>
{% endfor %}
</select>
<button type="submit" class="js-hidden btn btn-default">
@@ -31,24 +31,24 @@
</button>
</div>
<div class="col-sm-4 hidden-xs text-right flip">
<a href="?{% url_replace request "year" after.isocalendar.0 "week" after.isocalendar.1 %}"
<a href="?{% url_replace request "year" subevent_list.after.isocalendar.0 "week" subevent_list.after.isocalendar.1 %}"
class="btn btn-default">
{{ after|date:week_format }}
{{ subevent_list.after|date:subevent_list.week_format }}
<span class="fa fa-arrow-right" aria-hidden="true"></span>
</a>
</div>
</div>
</form>
{% 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 %}
<div class="col-sm-4 visible-xs text-center">
<a href="?{% url_replace request "year" before.isocalendar.0 "week" before.isocalendar.1 %}"
<a href="?{% url_replace request "year" subevent_list.before.isocalendar.0 "week" subevent_list.before.isocalendar.1 %}"
class="btn btn-default">
<span class="fa fa-arrow-left" aria-hidden="true"></span>
{{ before|date:week_format }}
{{ subevent_list.before|date:subevent_list.week_format }}
</a>
<a href="?{% url_replace request "year" after.isocalendar.0 "week" after.isocalendar.1 %}"
<a href="?{% url_replace request "year" subevent_list.after.isocalendar.0 "week" subevent_list.after.isocalendar.1 %}"
class="btn btn-default">
{{ after|date:week_format }}
{{ subevent_list.after|date:subevent_list.week_format }}
<span class="fa fa-arrow-right" aria-hidden="true"></span>
</a>
</div>

View File

@@ -1,6 +1,6 @@
{% load i18n %}
{% load eventurl %}
{% for subev in subevent_list %}
{% for subev in subevent_list.subevent_list %}
<a href="{% if request.GET.voucher %}{% eventurl event "presale:event.redeem" cart_namespace=cart_namespace %}?voucher={{ request.GET.voucher|urlencode }}&subevent={{ subev.pk }}{% else %}{% eventurl event "presale:event.index" subevent=subev.id cart_namespace=cart_namespace %}{% endif %}"
class="subevent-row">
<div class="row">

View File

@@ -2,6 +2,7 @@
{% load i18n %}
{% load l10n %}
{% load eventurl %}
{% load cache_large %}
{% load money %}
{% load thumb %}
{% load eventsignal %}
@@ -73,13 +74,15 @@
</div>
<div class="panel-body">
<div>
{% 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 %}
</div>
</div>
</div>

View File

@@ -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'):

View File

@@ -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":

View File

@@ -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