From da4357b07ba206106405ba6f893318768a535766 Mon Sep 17 00:00:00 2001 From: Kevin Chung Date: Thu, 13 Aug 2020 11:53:36 -0400 Subject: [PATCH] Make scoreboard caching only cache the score table (#1586) * Make scoreboard caching only cache the score table instead of the entire page * Closes #1584 --- CTFd/cache/__init__.py | 7 +++++-- CTFd/constants/__init__.py | 2 ++ CTFd/constants/static.py | 14 ++++++++++++++ CTFd/scoreboard.py | 2 -- CTFd/themes/core/templates/scoreboard.html | 2 ++ CTFd/utils/initialization/__init__.py | 12 ++++++++++++ tests/api/v1/test_scoreboard.py | 14 +++++++++++--- 7 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 CTFd/constants/static.py diff --git a/CTFd/cache/__init__.py b/CTFd/cache/__init__.py index a7f92d97..d452712f 100644 --- a/CTFd/cache/__init__.py +++ b/CTFd/cache/__init__.py @@ -1,5 +1,5 @@ from flask import request -from flask_caching import Cache +from flask_caching import Cache, make_template_fragment_key cache = Cache() @@ -27,6 +27,7 @@ def clear_config(): def clear_standings(): from CTFd.models import Users, Teams + from CTFd.constants.static import CacheKeys from CTFd.utils.scores import get_standings, get_team_standings, get_user_standings from CTFd.api.v1.scoreboard import ScoreboardDetail, ScoreboardList from CTFd.api import api @@ -55,11 +56,13 @@ def clear_standings(): cache.delete_memoized(get_team_place) # Clear out HTTP request responses - cache.delete(make_cache_key(path="scoreboard.listing")) cache.delete(make_cache_key(path=api.name + "." + ScoreboardList.endpoint)) cache.delete(make_cache_key(path=api.name + "." + ScoreboardDetail.endpoint)) cache.delete_memoized(ScoreboardList.get) + # Clear out scoreboard templates + cache.delete(make_template_fragment_key(CacheKeys.PUBLIC_SCOREBOARD_TABLE)) + def clear_pages(): from CTFd.utils.config.pages import get_page, get_pages diff --git a/CTFd/constants/__init__.py b/CTFd/constants/__init__.py index fc9a1baf..72b09b35 100644 --- a/CTFd/constants/__init__.py +++ b/CTFd/constants/__init__.py @@ -3,6 +3,7 @@ from enum import Enum from flask import current_app JS_ENUMS = {} +JINJA_ENUMS = {} class RawEnum(Enum): @@ -59,6 +60,7 @@ def JinjaEnum(cls): """ if cls.__name__ not in current_app.jinja_env.globals: current_app.jinja_env.globals[cls.__name__] = cls + JINJA_ENUMS[cls.__name__] = cls else: raise KeyError("{} was already defined as a JinjaEnum".format(cls.__name__)) return cls diff --git a/CTFd/constants/static.py b/CTFd/constants/static.py new file mode 100644 index 00000000..93a380f7 --- /dev/null +++ b/CTFd/constants/static.py @@ -0,0 +1,14 @@ +from CTFd.constants import JinjaEnum, RawEnum + + +@JinjaEnum +class CacheKeys(str, RawEnum): + PUBLIC_SCOREBOARD_TABLE = "public_scoreboard_table" + + +# Placeholder object. Not used, just imported to force initialization of any Enums here +class _StaticsWrapper: + pass + + +Static = _StaticsWrapper() diff --git a/CTFd/scoreboard.py b/CTFd/scoreboard.py index cb1bd88a..7e9b3409 100644 --- a/CTFd/scoreboard.py +++ b/CTFd/scoreboard.py @@ -1,6 +1,5 @@ from flask import Blueprint, render_template -from CTFd.cache import cache, make_cache_key from CTFd.utils import config from CTFd.utils.config.visibility import scores_visible from CTFd.utils.decorators.visibility import check_score_visibility @@ -13,7 +12,6 @@ scoreboard = Blueprint("scoreboard", __name__) @scoreboard.route("/scoreboard") @check_score_visibility -@cache.cached(timeout=60, key_prefix=make_cache_key) def listing(): infos = get_infos() diff --git a/CTFd/themes/core/templates/scoreboard.html b/CTFd/themes/core/templates/scoreboard.html index 9383829f..d657409f 100644 --- a/CTFd/themes/core/templates/scoreboard.html +++ b/CTFd/themes/core/templates/scoreboard.html @@ -15,6 +15,7 @@ + {% cache 60, CacheKeys.PUBLIC_SCOREBOARD_TABLE %} {% if standings %}
@@ -55,6 +56,7 @@
{% endif %} + {% endcache %} {% endblock %} diff --git a/CTFd/utils/initialization/__init__.py b/CTFd/utils/initialization/__init__.py index 1b61d168..cc948e8b 100644 --- a/CTFd/utils/initialization/__init__.py +++ b/CTFd/utils/initialization/__init__.py @@ -52,9 +52,11 @@ def init_template_filters(app): def init_template_globals(app): + from CTFd.constants import JINJA_ENUMS from CTFd.constants.config import Configs from CTFd.constants.plugins import Plugins from CTFd.constants.sessions import Session + from CTFd.constants.static import Static from CTFd.constants.users import User from CTFd.constants.teams import Team from CTFd.forms import Forms @@ -101,10 +103,20 @@ def init_template_globals(app): app.jinja_env.globals.update(Configs=Configs) app.jinja_env.globals.update(Plugins=Plugins) app.jinja_env.globals.update(Session=Session) + app.jinja_env.globals.update(Static=Static) app.jinja_env.globals.update(Forms=Forms) app.jinja_env.globals.update(User=User) app.jinja_env.globals.update(Team=Team) + # Add in JinjaEnums + # The reason this exists is that on double import, JinjaEnums are not reinitialized + # Thus, if you try to create two jinja envs (e.g. during testing), sometimes + # an Enum will not be available to Jinja. + # Instead we can just directly grab them from the persisted global dictionary. + for k, v in JINJA_ENUMS.items(): + # .update() can't be used here because it would use the literal value k + app.jinja_env.globals[k] = v + def init_logs(app): logger_submissions = logging.getLogger("submissions") diff --git a/tests/api/v1/test_scoreboard.py b/tests/api/v1/test_scoreboard.py index 5d774bfb..ec16811a 100644 --- a/tests/api/v1/test_scoreboard.py +++ b/tests/api/v1/test_scoreboard.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +from flask_caching import make_template_fragment_key + from CTFd.cache import clear_standings from tests.helpers import ( create_ctfd, @@ -40,13 +42,19 @@ def test_scoreboard_is_cached(): assert app.cache.get("view/api.scoreboard_scoreboard_detail") # Check scoreboard page - assert app.cache.get("view/scoreboard.listing") is None + assert ( + app.cache.get(make_template_fragment_key("public_scoreboard_table")) + is None + ) client.get("/scoreboard") - assert app.cache.get("view/scoreboard.listing") + assert app.cache.get(make_template_fragment_key("public_scoreboard_table")) # Empty standings and check that the cached data is gone clear_standings() assert app.cache.get("view/api.scoreboard_scoreboard_list") is None assert app.cache.get("view/api.scoreboard_scoreboard_detail") is None - assert app.cache.get("view/scoreboard.listing") is None + assert ( + app.cache.get(make_template_fragment_key("public_scoreboard_table")) + is None + ) destroy_ctfd(app)