173 lines
4.3 KiB
Python
173 lines
4.3 KiB
Python
# builtins
|
|
import functools
|
|
|
|
# third-party
|
|
import werkzeug.routing.map
|
|
import flask
|
|
|
|
# internals
|
|
import util
|
|
|
|
class URLMap(werkzeug.routing.map.Map):
|
|
|
|
def remove_endpoint(self, endpoint):
|
|
|
|
rules = []
|
|
for rule in self._rules:
|
|
if rule.endpoint == endpoint:
|
|
rules.append(rule)
|
|
|
|
for rule in rules:
|
|
self._rules.remove(rule)
|
|
|
|
del(self._rules_by_endpoint[endpoint])
|
|
|
|
self._remap = True
|
|
self.update()
|
|
|
|
class MenuMixin:
|
|
|
|
def menu(self, name):
|
|
|
|
def decorator(f):
|
|
|
|
self.menu_functions[name] = f
|
|
|
|
return f
|
|
|
|
return decorator
|
|
|
|
def menu_exists(self, name):
|
|
|
|
return name in self.menu_functions
|
|
|
|
def menu_get(self, name):
|
|
|
|
return self.menu_functions[name]()
|
|
|
|
class BlockMixin:
|
|
|
|
def block(self, name):
|
|
|
|
def decorator(f):
|
|
|
|
self.block_functions[name] = f
|
|
|
|
return f
|
|
|
|
return decorator
|
|
|
|
def block_get(self, name):
|
|
|
|
return self.block_functions[name]()
|
|
|
|
class Blueprint(flask.Blueprint, MenuMixin, BlockMixin):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
|
self.menu_functions = {}
|
|
self.block_functions = {}
|
|
|
|
class Application(flask.Flask, MenuMixin, BlockMixin):
|
|
|
|
url_map_class = URLMap
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self.boot_functions = []
|
|
self.cron_functions = []
|
|
self.menu_functions = {}
|
|
self.block_functions = {}
|
|
self.models_exposed = {}
|
|
self.models_abstract = []
|
|
self.admin = Blueprint('admin', __name__, url_prefix='/admin')
|
|
|
|
def boot(self, f):
|
|
|
|
self.boot_functions.append(f)
|
|
|
|
return f
|
|
|
|
def boot_run(self):
|
|
|
|
for func in self.boot_functions:
|
|
try:
|
|
func()
|
|
except Exception as e:
|
|
self.logger.error(f"Boot function {func} failed to run: {str(e)}")
|
|
|
|
def cron(self, f):
|
|
|
|
self.cron_functions.append(f)
|
|
|
|
return f
|
|
|
|
def expose(self, base=None, mode='full'):
|
|
|
|
"""
|
|
base: str, rule of the listing.
|
|
single views will be placed in a path below the listing
|
|
"""
|
|
def decorator(cls):
|
|
|
|
import admin
|
|
import rendering
|
|
|
|
if cls.__name__ in self.models_exposed:
|
|
raise TypeError(f"A class with name {cls.__name} is already exposed.")
|
|
|
|
if not issubclass(cls, admin.Administerable):
|
|
raise TypeError("Can only expose classes derived from Administerable.")
|
|
|
|
if base is None:
|
|
rule_base = f'/{cls._lowerclass}/'
|
|
else:
|
|
rule_base = base
|
|
|
|
if rule_base[-1] != '/':
|
|
raise ValueError("base for @expose MUST end with '/'. Leave it empty to autogenerate based on class name.")
|
|
|
|
self.models_exposed[cls.__name__] = cls
|
|
|
|
# public routes
|
|
@rendering.page(title_show=False)
|
|
def listing(offset=0):
|
|
# TODO: Menu?
|
|
return rendering.Listing(cls.list(), title=cls.__name__, endpoint=f'{cls._lowerclass}_listing', offset=offset)
|
|
|
|
@rendering.page(format='atom')
|
|
def feed():
|
|
return rendering.Listing(cls.list(), title=cls.__name__)
|
|
|
|
self.add_url_rule(rule_base, f'{cls._lowerclass}_listing', listing)
|
|
self.add_url_rule(f'{rule_base}/+<int:offset>/', f'{cls._lowerclass}_listing', listing)
|
|
self.add_url_rule(f'/feed{rule_base}', f'{cls._lowerclass}_feed', feed)
|
|
|
|
if issubclass(cls, admin.Named):
|
|
self.add_url_rule(f'{rule_base}/<string:name>/', f'{cls._lowerclass}_full', functools.partial(cls.view, mode=mode))
|
|
else:
|
|
self.add_url_rule(f'{rule_base}/<int:id>/', f'{cls._lowerclass}_full', functools.partial(cls.view, mode=mode))
|
|
|
|
return cls
|
|
|
|
return decorator
|
|
|
|
def abstract(self, cls):
|
|
|
|
"""
|
|
Marks a class as abstract. Kind of a hack, should ideally be part of
|
|
ChildAware, but was easier to implement.
|
|
"""
|
|
|
|
self.models_abstract.append(cls)
|
|
|
|
return cls
|
|
|
|
app = Application('pdn', template_folder='themes')
|
|
|
|
app.config.from_pyfile('config.py')
|
|
app.jinja_env.filters['prettydate'] = util.prettydate
|