Add code to support integration with a Vite build system for JS/CSS (#2051)

* Adds the `Assets` constant to access front end assets from Jinja templates
* Adds a `views.themes_beta` route to avoid the `.dev`/`.min` extension being added automatically to frontend asset urls
* Add `count` meta field to `/api/v1/users/me/solves`, `/api/v1/users/me/fails`, `/api/v1/users/me/awards`, `/api/v1/users/[user_id]/solves`, `/api/v1/users/[user_id]/fails`, `/api/v1/users/[user_id]/awards`

* Works on #2049
This commit is contained in:
Kevin Chung
2022-04-04 22:59:13 -04:00
committed by GitHub
parent a2e7a32754
commit eb8461cf2f
6 changed files with 96 additions and 7 deletions

View File

@@ -339,7 +339,8 @@ class UserPrivateSolves(Resource):
if response.errors: if response.errors:
return {"success": False, "errors": response.errors}, 400 return {"success": False, "errors": response.errors}, 400
return {"success": True, "data": response.data} count = len(response.data)
return {"success": True, "data": response.data, "meta": {"count": count}}
@users_namespace.route("/me/fails") @users_namespace.route("/me/fails")
@@ -358,8 +359,8 @@ class UserPrivateFails(Resource):
data = response.data data = response.data
else: else:
data = [] data = []
count = len(response.data)
count = len(response.data)
return {"success": True, "data": data, "meta": {"count": count}} return {"success": True, "data": data, "meta": {"count": count}}
@@ -377,7 +378,8 @@ class UserPrivateAwards(Resource):
if response.errors: if response.errors:
return {"success": False, "errors": response.errors}, 400 return {"success": False, "errors": response.errors}, 400
return {"success": True, "data": response.data} count = len(response.data)
return {"success": True, "data": response.data, "meta": {"count": count}}
@users_namespace.route("/<user_id>/solves") @users_namespace.route("/<user_id>/solves")
@@ -399,7 +401,8 @@ class UserPublicSolves(Resource):
if response.errors: if response.errors:
return {"success": False, "errors": response.errors}, 400 return {"success": False, "errors": response.errors}, 400
return {"success": True, "data": response.data} count = len(response.data)
return {"success": True, "data": response.data, "meta": {"count": count}}
@users_namespace.route("/<user_id>/fails") @users_namespace.route("/<user_id>/fails")
@@ -423,8 +426,8 @@ class UserPublicFails(Resource):
data = response.data data = response.data
else: else:
data = [] data = []
count = len(response.data)
count = len(response.data)
return {"success": True, "data": data, "meta": {"count": count}} return {"success": True, "data": data, "meta": {"count": count}}
@@ -446,7 +449,8 @@ class UserPublicAwards(Resource):
if response.errors: if response.errors:
return {"success": False, "errors": response.errors}, 400 return {"success": False, "errors": response.errors}, 400
return {"success": True, "data": response.data} count = len(response.data)
return {"success": True, "data": response.data, "meta": {"count": count}}
@users_namespace.route("/<int:user_id>/email") @users_namespace.route("/<int:user_id>/email")

51
CTFd/constants/assets.py Normal file
View File

@@ -0,0 +1,51 @@
import os
from flask import current_app, url_for
from CTFd.utils import _get_asset_json
from CTFd.utils.config import ctf_theme
from CTFd.utils.helpers import markup
class _AssetsWrapper:
def manifest(self):
theme = ctf_theme()
manifest = os.path.join(
current_app.root_path, "themes", theme, "static", "manifest.json"
)
return _get_asset_json(path=manifest)
def manifest_css(self):
theme = ctf_theme()
manifest = os.path.join(
current_app.root_path, "themes", theme, "static", "manifest-css.json"
)
return _get_asset_json(path=manifest)
def js(self, asset_key):
asset = self.manifest()[asset_key]
entry = asset["file"]
imports = asset.get("imports", [])
html = ""
for i in imports:
# TODO: Needs a better recursive solution
i = self.manifest()[i]["file"]
url = url_for("views.themes_beta", path=i)
html += f'<script defer type="module" src="{url}"></script>'
url = url_for("views.themes_beta", path=entry)
html += f'<script defer type="module" src="{url}"></script>'
return markup(html)
def css(self, asset_key):
asset = self.manifest_css()[asset_key]
entry = asset["file"]
url = url_for("views.themes_beta", path=entry)
return markup(f'<link rel="stylesheet" href="{url}">')
def file(self, asset_key):
asset = self.manifest()[asset_key]
entry = asset["file"]
return url_for("views.themes_beta", path=entry)
Assets = _AssetsWrapper()

View File

@@ -1,3 +1,4 @@
import json
from enum import Enum from enum import Enum
import cmarkgfm import cmarkgfm
@@ -26,6 +27,12 @@ def get_app_config(key, default=None):
return value return value
@cache.memoize()
def _get_asset_json(path):
with open(path) as f:
return json.loads(f.read())
@cache.memoize() @cache.memoize()
def _get_config(key): def _get_config(key):
config = db.session.execute( config = db.session.execute(

View File

@@ -29,7 +29,12 @@ def get_errors():
@current_app.url_defaults @current_app.url_defaults
def env_asset_url_default(endpoint, values): def env_asset_url_default(endpoint, values):
"""Create asset URLs dependent on the current env""" """
Create asset URLs dependent on the current env
In CTFd 4.0 this url_for behavior and the themes_beta
route will be removed in favor of an improved theme system
"""
if endpoint == "views.themes": if endpoint == "views.themes":
path = values.get("path", "") path = values.get("path", "")
static_asset = path.endswith(".js") or path.endswith(".css") static_asset = path.endswith(".js") or path.endswith(".css")

View File

@@ -53,6 +53,7 @@ def init_template_filters(app):
def init_template_globals(app): def init_template_globals(app):
from CTFd.constants import JINJA_ENUMS from CTFd.constants import JINJA_ENUMS
from CTFd.constants.assets import Assets
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
@@ -101,6 +102,7 @@ def init_template_globals(app):
app.jinja_env.globals.update(get_current_user_attrs=get_current_user_attrs) app.jinja_env.globals.update(get_current_user_attrs=get_current_user_attrs)
app.jinja_env.globals.update(get_current_team_attrs=get_current_team_attrs) app.jinja_env.globals.update(get_current_team_attrs=get_current_team_attrs)
app.jinja_env.globals.update(get_ip=get_ip) app.jinja_env.globals.update(get_ip=get_ip)
app.jinja_env.globals.update(Assets=Assets)
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)

View File

@@ -484,3 +484,23 @@ def themes(theme, path):
if os.path.isfile(cand_path): if os.path.isfile(cand_path):
return send_file(cand_path) return send_file(cand_path)
abort(404) abort(404)
@views.route("/themes/<theme>/static/<path:path>")
def themes_beta(theme, path):
"""
This is a copy of the above themes route used to avoid
the current appending of .dev and .min for theme assets.
In CTFd 4.0 this url_for behavior and this themes_beta
route will be removed.
"""
for cand_path in (
safe_join(app.root_path, "themes", cand_theme, "static", path)
# The `theme` value passed in may not be the configured one, e.g. for
# admin pages, so we check that first
for cand_theme in (theme, *config.ctf_theme_candidates())
):
if os.path.isfile(cand_path):
return send_file(cand_path)
abort(404)