pdnew/application.py

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