forked from CGM_Public/pretix_original
Performance: Cache complete organizer index page, cache subevent list template fragment (#2125)
This commit is contained in:
35
src/pretix/base/templatetags/cache_large.py
Normal file
35
src/pretix/base/templatetags/cache_large.py
Normal 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)),
|
||||
)
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'):
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user