Make scoreboard caching only cache the score table (#1586)

* Make scoreboard caching only cache the score table instead of the entire page
* Closes #1584
This commit is contained in:
Kevin Chung
2020-08-13 11:53:36 -04:00
committed by GitHub
parent 58e38c4bde
commit da4357b07b
7 changed files with 46 additions and 7 deletions

View File

@@ -1,5 +1,5 @@
from flask import request from flask import request
from flask_caching import Cache from flask_caching import Cache, make_template_fragment_key
cache = Cache() cache = Cache()
@@ -27,6 +27,7 @@ def clear_config():
def clear_standings(): def clear_standings():
from CTFd.models import Users, Teams 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.utils.scores import get_standings, get_team_standings, get_user_standings
from CTFd.api.v1.scoreboard import ScoreboardDetail, ScoreboardList from CTFd.api.v1.scoreboard import ScoreboardDetail, ScoreboardList
from CTFd.api import api from CTFd.api import api
@@ -55,11 +56,13 @@ def clear_standings():
cache.delete_memoized(get_team_place) cache.delete_memoized(get_team_place)
# Clear out HTTP request responses # 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 + "." + ScoreboardList.endpoint))
cache.delete(make_cache_key(path=api.name + "." + ScoreboardDetail.endpoint)) cache.delete(make_cache_key(path=api.name + "." + ScoreboardDetail.endpoint))
cache.delete_memoized(ScoreboardList.get) cache.delete_memoized(ScoreboardList.get)
# Clear out scoreboard templates
cache.delete(make_template_fragment_key(CacheKeys.PUBLIC_SCOREBOARD_TABLE))
def clear_pages(): def clear_pages():
from CTFd.utils.config.pages import get_page, get_pages from CTFd.utils.config.pages import get_page, get_pages

View File

@@ -3,6 +3,7 @@ from enum import Enum
from flask import current_app from flask import current_app
JS_ENUMS = {} JS_ENUMS = {}
JINJA_ENUMS = {}
class RawEnum(Enum): class RawEnum(Enum):
@@ -59,6 +60,7 @@ def JinjaEnum(cls):
""" """
if cls.__name__ not in current_app.jinja_env.globals: if cls.__name__ not in current_app.jinja_env.globals:
current_app.jinja_env.globals[cls.__name__] = cls current_app.jinja_env.globals[cls.__name__] = cls
JINJA_ENUMS[cls.__name__] = cls
else: else:
raise KeyError("{} was already defined as a JinjaEnum".format(cls.__name__)) raise KeyError("{} was already defined as a JinjaEnum".format(cls.__name__))
return cls return cls

14
CTFd/constants/static.py Normal file
View File

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

View File

@@ -1,6 +1,5 @@
from flask import Blueprint, render_template from flask import Blueprint, render_template
from CTFd.cache import cache, make_cache_key
from CTFd.utils import config from CTFd.utils import config
from CTFd.utils.config.visibility import scores_visible from CTFd.utils.config.visibility import scores_visible
from CTFd.utils.decorators.visibility import check_score_visibility from CTFd.utils.decorators.visibility import check_score_visibility
@@ -13,7 +12,6 @@ scoreboard = Blueprint("scoreboard", __name__)
@scoreboard.route("/scoreboard") @scoreboard.route("/scoreboard")
@check_score_visibility @check_score_visibility
@cache.cached(timeout=60, key_prefix=make_cache_key)
def listing(): def listing():
infos = get_infos() infos = get_infos()

View File

@@ -15,6 +15,7 @@
</div> </div>
</div> </div>
{% cache 60, CacheKeys.PUBLIC_SCOREBOARD_TABLE %}
{% if standings %} {% if standings %}
<div id="scoreboard" class="row"> <div id="scoreboard" class="row">
<div class="col-md-12"> <div class="col-md-12">
@@ -55,6 +56,7 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% endcache %}
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -52,9 +52,11 @@ def init_template_filters(app):
def init_template_globals(app): def init_template_globals(app):
from CTFd.constants import JINJA_ENUMS
from CTFd.constants.config import Configs from CTFd.constants.config import Configs
from CTFd.constants.plugins import Plugins from CTFd.constants.plugins import Plugins
from CTFd.constants.sessions import Session from CTFd.constants.sessions import Session
from CTFd.constants.static import Static
from CTFd.constants.users import User from CTFd.constants.users import User
from CTFd.constants.teams import Team from CTFd.constants.teams import Team
from CTFd.forms import Forms 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(Configs=Configs)
app.jinja_env.globals.update(Plugins=Plugins) app.jinja_env.globals.update(Plugins=Plugins)
app.jinja_env.globals.update(Session=Session) 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(Forms=Forms)
app.jinja_env.globals.update(User=User) app.jinja_env.globals.update(User=User)
app.jinja_env.globals.update(Team=Team) 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): def init_logs(app):
logger_submissions = logging.getLogger("submissions") logger_submissions = logging.getLogger("submissions")

View File

@@ -1,6 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from flask_caching import make_template_fragment_key
from CTFd.cache import clear_standings from CTFd.cache import clear_standings
from tests.helpers import ( from tests.helpers import (
create_ctfd, create_ctfd,
@@ -40,13 +42,19 @@ def test_scoreboard_is_cached():
assert app.cache.get("view/api.scoreboard_scoreboard_detail") assert app.cache.get("view/api.scoreboard_scoreboard_detail")
# Check scoreboard page # 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") 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 # Empty standings and check that the cached data is gone
clear_standings() clear_standings()
assert app.cache.get("view/api.scoreboard_scoreboard_list") is None 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/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) destroy_ctfd(app)