Better constant value management (#1335)

* Starts work on #929 
* Adds Enum classes that can be accessed from JS, Jinja, and Python code. This allows for the sharing of constant values between the three major codebases in CTFd.
This commit is contained in:
Kevin Chung
2020-04-23 10:48:09 -04:00
committed by GitHub
parent fa434c4bdd
commit 1f87efb6c1
3 changed files with 137 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
from enum import Enum
from flask import current_app
JS_ENUMS = {}
class RawEnum(Enum):
"""
This is a customized enum class which should be used with a mixin.
The mixin should define the types of each member.
For example:
class Colors(str, RawEnum):
RED = "red"
GREEN = "green"
BLUE = "blue"
"""
def __str__(self):
return str(self._value_)
@classmethod
def keys(cls):
return list(cls.__members__.keys())
@classmethod
def values(cls):
return list(cls.__members__.values())
@classmethod
def test(cls, value):
try:
return bool(cls(value))
except ValueError:
return False
def JSEnum(cls):
"""
This is a decorator used to gather all Enums which should be shared with
the CTFd front end. The JS_Enums dictionary can be taken be a script and
compiled into a JavaScript file for use by frontend assets. JS_Enums
should not be passed directly into Jinja. A JinjaEnum is better for that.
"""
if cls.__name__ not in JS_ENUMS:
JS_ENUMS[cls.__name__] = dict(cls.__members__)
else:
raise KeyError("{} was already defined as a JSEnum".format(cls.__name__))
return cls
def JinjaEnum(cls):
"""
This is a decorator used to inject the decorated Enum into Jinja globals
which allows you to access it from the front end. If you need to access
an Enum from JS, a better tool to use is the JSEnum decorator.
"""
if cls.__name__ not in current_app.jinja_env.globals:
current_app.jinja_env.globals[cls.__name__] = cls
else:
raise KeyError("{} was already defined as a JinjaEnum".format(cls.__name__))
return cls

View File

@@ -12,6 +12,21 @@ manager = Manager(app)
manager.add_command("db", MigrateCommand)
def jsenums():
from CTFd.constants import JS_ENUMS
import json
import os
path = os.path.join(app.root_path, "themes/core/assets/js/constants.js")
with open(path, "w+") as f:
for k, v in JS_ENUMS.items():
f.write("const {} = Object.freeze({});".format(k, json.dumps(v)))
BUILD_COMMANDS = {"jsenums": jsenums}
@manager.command
def get_config(key):
with app.app_context():
@@ -24,5 +39,12 @@ def set_config(key, value):
print(set_config_util(key, value).value)
@manager.command
def build(cmd):
with app.app_context():
cmd = BUILD_COMMANDS.get(cmd)
cmd()
if __name__ == "__main__":
manager.run()

View File

@@ -0,0 +1,52 @@
from CTFd.constants import RawEnum, JSEnum, JinjaEnum
from tests.helpers import create_ctfd, destroy_ctfd
def test_RawEnum():
class Colors(str, RawEnum):
RED = "red"
GREEN = "green"
BLUE = "blue"
class Numbers(str, RawEnum):
ONE = 1
TWO = 2
THREE = 3
assert Colors.RED == "red"
assert Colors.GREEN == "green"
assert Colors.BLUE == "blue"
assert Colors.test("red") is True
assert Colors.test("purple") is False
assert str(Numbers.ONE) == "1"
assert sorted(Colors.keys()) == sorted(["RED", "GREEN", "BLUE"])
assert sorted(Colors.values()) == sorted(["red", "green", "blue"])
def test_JSEnum():
from CTFd.constants import JS_ENUMS
import json
@JSEnum
class Colors(str, RawEnum):
RED = "red"
GREEN = "green"
BLUE = "blue"
assert JS_ENUMS["Colors"] == {"RED": "red", "GREEN": "green", "BLUE": "blue"}
assert json.dumps(JS_ENUMS)
def test_JinjaEnum():
app = create_ctfd()
with app.app_context():
@JinjaEnum
class Colors(str, RawEnum):
RED = "red"
GREEN = "green"
BLUE = "blue"
assert app.jinja_env.globals["Colors"] is Colors
assert app.jinja_env.globals["Colors"].RED == "red"
destroy_ctfd(app)