# 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}/+/', 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}//', f'{cls._lowerclass}_full', functools.partial(cls.view, mode=mode)) else: self.add_url_rule(f'{rule_base}//', 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