2024-04-12 19:22:00 +00:00
|
|
|
|
# builtins
|
2024-05-13 02:52:45 +00:00
|
|
|
|
import math
|
2024-04-22 21:29:28 +00:00
|
|
|
|
import os
|
2024-04-12 19:22:00 +00:00
|
|
|
|
import re
|
|
|
|
|
|
|
|
|
|
# third-party
|
2024-04-12 19:22:00 +00:00
|
|
|
|
import markupsafe
|
2024-04-12 19:22:00 +00:00
|
|
|
|
import werkzeug
|
|
|
|
|
import flask
|
2024-05-13 02:52:45 +00:00
|
|
|
|
import peewee # for type checking
|
2024-04-12 19:22:00 +00:00
|
|
|
|
|
|
|
|
|
# internals
|
|
|
|
|
from application import app
|
|
|
|
|
|
|
|
|
|
import util
|
|
|
|
|
|
|
|
|
|
def page(**template_params):
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
Decorator for endpoint functions to render the content returned by the
|
|
|
|
|
endpoint function within a proper layout.
|
|
|
|
|
Takes keyword arguments that will be passed into the layout template.
|
|
|
|
|
If the returned content is a Renderable, the layout will render it
|
|
|
|
|
with the passed `mode` if any or 'full' mode otherwise.
|
|
|
|
|
|
|
|
|
|
ex:
|
|
|
|
|
```
|
|
|
|
|
@app.route('/some/path/')
|
|
|
|
|
@page(title="Some page")
|
|
|
|
|
def some_endpoint():
|
|
|
|
|
return "Some content"
|
|
|
|
|
```
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def decorator(f):
|
|
|
|
|
|
|
|
|
|
def wrapper(*args, **kwargs):
|
|
|
|
|
|
2024-04-23 13:42:01 +00:00
|
|
|
|
params = {
|
2024-05-20 02:10:17 +00:00
|
|
|
|
'site_name': app.config['SITE_NAME'],
|
2024-04-23 13:42:01 +00:00
|
|
|
|
'user': flask.g.user,
|
2024-05-20 02:10:17 +00:00
|
|
|
|
'tls_cipher': flask.g.tls_cipher,
|
|
|
|
|
'client_cert_verified': flask.g.client_cert_verified,
|
|
|
|
|
'client_cert_fingerprint': flask.g.client_cert_fingerprint,
|
|
|
|
|
'client_cert_fingerprint_matched': flask.g.client_cert_fingerprint_matched,
|
2024-04-23 13:42:01 +00:00
|
|
|
|
}
|
|
|
|
|
params.update(template_params)
|
|
|
|
|
|
|
|
|
|
if 'format' not in params:
|
|
|
|
|
params['format'] = 'html'
|
|
|
|
|
|
2024-04-12 19:22:00 +00:00
|
|
|
|
if 'layout' in kwargs:
|
|
|
|
|
|
|
|
|
|
# explicitly chosen layout
|
|
|
|
|
|
|
|
|
|
template_candidates = [
|
2024-04-23 13:42:01 +00:00
|
|
|
|
f'{app.config["THEME"]}/templates/layout/{params["layout"]}.{params["format"]}',
|
2024-04-12 19:22:00 +00:00
|
|
|
|
]
|
2024-04-12 19:22:00 +00:00
|
|
|
|
|
2024-04-12 19:22:00 +00:00
|
|
|
|
if app.config['THEME'] != 'default':
|
2024-04-23 13:42:01 +00:00
|
|
|
|
template_candidates.append(f'default/templates/layout/{params["layout"]}.{params["format"]}')
|
2024-04-12 19:22:00 +00:00
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
|
|
# choose layout by endpoint name if a fit exists,
|
|
|
|
|
# otherwise default to 'main'.
|
|
|
|
|
|
|
|
|
|
template_candidates = [
|
2024-04-23 13:42:01 +00:00
|
|
|
|
f'{app.config["THEME"]}/templates/layout/{f.__name__}.{params["format"]}',
|
|
|
|
|
f'{app.config["THEME"]}/templates/layout/main.{params["format"]}',
|
2024-04-12 19:22:00 +00:00
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
if app.config['THEME'] != 'default':
|
2024-04-23 13:42:01 +00:00
|
|
|
|
template_candidates.append(f'default/templates/layout/{f.__name__}.{params["format"]}')
|
|
|
|
|
template_candidates.append('default/templates/layout/main.{params["format"]}')
|
2024-04-12 19:22:00 +00:00
|
|
|
|
|
|
|
|
|
content = f(*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
if isinstance(content, werkzeug.Response):
|
|
|
|
|
# if the original return value of the decorated function is
|
|
|
|
|
# a response object, return it as-is – useful for redirects
|
|
|
|
|
return content
|
|
|
|
|
|
2024-04-23 13:42:01 +00:00
|
|
|
|
params['content'] = content
|
2024-04-12 19:22:00 +00:00
|
|
|
|
|
2024-04-12 19:22:00 +00:00
|
|
|
|
if flask.request.blueprint:
|
|
|
|
|
blueprint = app.blueprints[flask.request.blueprint]
|
|
|
|
|
else:
|
|
|
|
|
blueprint = None
|
2024-04-12 19:22:00 +00:00
|
|
|
|
|
2024-04-12 19:22:00 +00:00
|
|
|
|
|
2024-05-13 03:57:53 +00:00
|
|
|
|
for menu_name in app.config['MENUS']:
|
2024-04-12 19:22:00 +00:00
|
|
|
|
|
2024-05-13 03:57:53 +00:00
|
|
|
|
try:
|
|
|
|
|
if blueprint and blueprint.menu_exists(menu_name):
|
|
|
|
|
params[f'menu_{menu_name}'] = blueprint.menu_get(menu_name)
|
|
|
|
|
elif app.menu_exists(menu_name):
|
|
|
|
|
params[f'menu_{menu_name}'] = app.menu_get(menu_name)
|
2024-04-12 19:22:00 +00:00
|
|
|
|
|
2024-05-13 03:57:53 +00:00
|
|
|
|
except Exception as e:
|
2024-04-12 19:22:00 +00:00
|
|
|
|
|
2024-05-13 03:57:53 +00:00
|
|
|
|
if app.debug:
|
|
|
|
|
raise
|
2024-04-12 19:22:00 +00:00
|
|
|
|
|
2024-05-13 03:57:53 +00:00
|
|
|
|
app.logger.error(f"Error generating menu '{menu_name}': {str(e)}")
|
|
|
|
|
|
|
|
|
|
block_source = blueprint or app
|
|
|
|
|
|
|
|
|
|
for block_name in block_source.block_functions:
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
|
|
params[f'block_{block_name}'] = block_source.block_get(block_name)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
|
|
|
|
if app.debug:
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
app.logger.error(f"(Error generating block '{block_name}': {str(e)}")
|
2024-04-12 19:22:00 +00:00
|
|
|
|
|
2024-04-12 19:22:00 +00:00
|
|
|
|
if 'mode' not in params:
|
|
|
|
|
params['mode'] = 'full'
|
2024-04-12 19:22:00 +00:00
|
|
|
|
|
2024-04-12 19:22:00 +00:00
|
|
|
|
if 'title' not in params: # means it was passed directly in **template_params
|
2024-04-12 19:22:00 +00:00
|
|
|
|
|
2024-04-12 19:22:00 +00:00
|
|
|
|
if type(content) != str and\
|
|
|
|
|
hasattr(content, 'title') and\
|
|
|
|
|
content.title is not None:
|
|
|
|
|
params['title'] = content.title
|
|
|
|
|
else:
|
2024-04-23 08:17:44 +00:00
|
|
|
|
params['title'] = None
|
2024-04-12 19:22:00 +00:00
|
|
|
|
|
2024-04-12 19:22:00 +00:00
|
|
|
|
if 'title_show' not in params:
|
|
|
|
|
params['title_show'] = True
|
|
|
|
|
|
|
|
|
|
status = content.status if isinstance(content, ErrorPage) else 200
|
2024-04-12 19:22:00 +00:00
|
|
|
|
|
2024-04-23 13:42:01 +00:00
|
|
|
|
body = flask.render_template(
|
2024-04-12 19:22:00 +00:00
|
|
|
|
template_candidates,
|
|
|
|
|
**params
|
2024-04-23 13:42:01 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
response = flask.Response(body, status)
|
|
|
|
|
|
|
|
|
|
if params['format'] in app.config['PAGE_CONTENT_TYPES']:
|
|
|
|
|
response.headers['Content-Type'] = app.config['PAGE_CONTENT_TYPES'][params['format']]
|
|
|
|
|
|
|
|
|
|
return response
|
2024-04-12 19:22:00 +00:00
|
|
|
|
|
|
|
|
|
wrapper.__name__ = f.__name__ # avoid name collisions in routing data
|
|
|
|
|
return wrapper
|
|
|
|
|
|
|
|
|
|
return decorator
|
|
|
|
|
|
|
|
|
|
@app.template_test('renderable')
|
|
|
|
|
def test_renderable(obj):
|
|
|
|
|
return isinstance(obj, Renderable)
|
|
|
|
|
|
2024-04-22 21:29:28 +00:00
|
|
|
|
@app.route('/theme/<path:asset>')
|
|
|
|
|
def serve_asset(asset):
|
|
|
|
|
|
|
|
|
|
asset_parts = asset.split('/')
|
|
|
|
|
filename = asset_parts[-1]
|
|
|
|
|
subdirectory_parts = asset_parts[:-1]
|
|
|
|
|
subdirectory = '/'.join(subdirectory_parts)
|
|
|
|
|
|
|
|
|
|
directory_candidates = [
|
|
|
|
|
os.path.join('themes', app.config['THEME'], 'assets', subdirectory)
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
if app.config['THEME'] != 'default':
|
|
|
|
|
directory_candidates.append(os.path.join('themes', 'default', 'assets', subdirectory))
|
|
|
|
|
|
|
|
|
|
for directory_candidate in directory_candidates:
|
|
|
|
|
|
|
|
|
|
path_candidate = os.path.join(directory_candidate, filename)
|
|
|
|
|
|
|
|
|
|
if os.path.exists(path_candidate):
|
|
|
|
|
return flask.send_from_directory(directory_candidate, filename)
|
|
|
|
|
|
|
|
|
|
flask.abort(404)
|
|
|
|
|
|
2024-04-12 19:22:00 +00:00
|
|
|
|
class Renderable(util.ChildAware):
|
|
|
|
|
|
2024-06-29 03:36:26 +00:00
|
|
|
|
def __init__(self, extra_classes=None):
|
|
|
|
|
|
|
|
|
|
self.extra_classes = extra_classes or []
|
|
|
|
|
|
2024-04-14 13:56:36 +00:00
|
|
|
|
@classmethod
|
|
|
|
|
def load(cls, **kwargs):
|
|
|
|
|
|
|
|
|
|
# this is essentially an alias for instantiation to keep consistent
|
|
|
|
|
# with Administerable.load
|
|
|
|
|
return cls(**kwargs)
|
|
|
|
|
|
2024-04-23 13:42:01 +00:00
|
|
|
|
def template_candidates(self, mode, format):
|
2024-04-12 19:22:00 +00:00
|
|
|
|
|
|
|
|
|
candidates = []
|
|
|
|
|
|
|
|
|
|
for cls in self.__class__.mro():
|
|
|
|
|
if issubclass(cls, Renderable):
|
|
|
|
|
candidates += [
|
2024-04-23 13:42:01 +00:00
|
|
|
|
f'{app.config["THEME"]}/templates/renderable/{cls._lowerclass}-{mode}.{format}',
|
|
|
|
|
f'{app.config["THEME"]}/templates/renderable/{cls._lowerclass}.{format}',
|
2024-04-12 19:22:00 +00:00
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
if app.config['THEME'] != 'default':
|
|
|
|
|
for cls in self.__class__.mro():
|
|
|
|
|
if issubclass(cls, Renderable):
|
|
|
|
|
candidates += [
|
2024-04-23 13:42:01 +00:00
|
|
|
|
f'default/templates/renderable/{self._lowerclass}-{mode}.{format}',
|
|
|
|
|
f'default/templates/renderable/{self._lowerclass}.{format}',
|
2024-04-12 19:22:00 +00:00
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
return candidates
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def css_classes(self):
|
|
|
|
|
classes = []
|
|
|
|
|
for cls in self.__class__.mro():
|
|
|
|
|
if issubclass(cls, Renderable):
|
|
|
|
|
classes.append(cls._lowerclass)
|
|
|
|
|
|
2024-06-29 03:36:26 +00:00
|
|
|
|
return ' '.join([*classes, *self.extra_classes])
|
2024-04-12 19:22:00 +00:00
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def view(cls, mode='full', **kwargs):
|
|
|
|
|
|
|
|
|
|
@page(mode=mode)
|
|
|
|
|
def wrapped(**kw):
|
2024-07-13 21:09:48 +00:00
|
|
|
|
|
|
|
|
|
instance = cls(**kw)
|
|
|
|
|
|
|
|
|
|
if not instance.access(mode):
|
|
|
|
|
flask.abort(403)
|
|
|
|
|
|
|
|
|
|
return instance
|
2024-04-12 19:22:00 +00:00
|
|
|
|
|
|
|
|
|
return wrapped(**kwargs)
|
|
|
|
|
|
2024-07-13 21:09:48 +00:00
|
|
|
|
def access(self, mode='full'):
|
|
|
|
|
return True
|
|
|
|
|
|
2024-04-23 13:42:01 +00:00
|
|
|
|
def render(self, mode='full', format='html'):
|
2024-04-12 19:22:00 +00:00
|
|
|
|
|
2024-04-12 19:22:00 +00:00
|
|
|
|
return markupsafe.Markup(flask.render_template(
|
2024-04-23 13:42:01 +00:00
|
|
|
|
self.template_candidates(mode, format),
|
2024-04-12 19:22:00 +00:00
|
|
|
|
content=self,
|
2024-04-23 13:42:01 +00:00
|
|
|
|
mode=mode,
|
|
|
|
|
format=format,
|
2024-04-12 19:22:00 +00:00
|
|
|
|
))
|
2024-04-12 19:22:00 +00:00
|
|
|
|
|
|
|
|
|
class Menu(Renderable):
|
|
|
|
|
|
2024-06-29 03:36:26 +00:00
|
|
|
|
def __init__(self, items=None, id=None, **kwargs):
|
|
|
|
|
|
|
|
|
|
super().__init__(**kwargs)
|
2024-04-12 19:22:00 +00:00
|
|
|
|
|
|
|
|
|
self.items = []
|
|
|
|
|
self.id = id
|
|
|
|
|
|
|
|
|
|
if items:
|
|
|
|
|
for item in items:
|
|
|
|
|
self.append(**item)
|
|
|
|
|
|
2024-05-13 02:52:45 +00:00
|
|
|
|
def append(self, url=None, label=None, endpoint=None, params=None, classes=None):
|
2024-04-12 19:22:00 +00:00
|
|
|
|
|
|
|
|
|
if not url and not endpoint:
|
|
|
|
|
raise TypeError("Menu items MUST include either 'url' or 'endpoint'.")
|
|
|
|
|
if url and endpoint:
|
|
|
|
|
raise TypeError("Menu items MUST include either 'url' or 'endpoint', but not both.")
|
|
|
|
|
if url and params:
|
|
|
|
|
raise TypeError("Menu items with 'url' MUST NOT include 'params', which is for endpoint items only.")
|
|
|
|
|
if not label:
|
|
|
|
|
raise TypeError("Menu items MUST include 'label'.")
|
|
|
|
|
|
|
|
|
|
if endpoint:
|
|
|
|
|
if params is None:
|
|
|
|
|
params = {}
|
|
|
|
|
|
|
|
|
|
url = flask.url_for(endpoint, **params)
|
|
|
|
|
|
|
|
|
|
state = 'inactive'
|
|
|
|
|
if flask.has_request_context():
|
|
|
|
|
|
|
|
|
|
if flask.request.url_rule is not None: # ignore requests for which no route exists
|
|
|
|
|
if flask.request.url_rule.endpoint == endpoint:
|
|
|
|
|
state = 'active'
|
|
|
|
|
|
2024-05-23 21:27:21 +00:00
|
|
|
|
elif endpoint != 'frontpage': # determine trailing status, but not for "home" page.
|
2024-04-12 19:22:00 +00:00
|
|
|
|
|
2024-05-14 02:49:51 +00:00
|
|
|
|
if flask.request.path.startswith(url):
|
|
|
|
|
state = 'trail'
|
|
|
|
|
else:
|
|
|
|
|
state = 'inactive'
|
2024-04-12 19:22:00 +00:00
|
|
|
|
|
|
|
|
|
self.items.append({
|
|
|
|
|
'url': url,
|
|
|
|
|
'endpoint': endpoint,
|
|
|
|
|
'state': state,
|
2024-05-13 02:52:45 +00:00
|
|
|
|
'label': label,
|
|
|
|
|
'classes': classes
|
2024-04-12 19:22:00 +00:00
|
|
|
|
})
|
|
|
|
|
|
2024-05-13 02:52:45 +00:00
|
|
|
|
class Pagination(Renderable):
|
|
|
|
|
|
2024-06-29 03:36:26 +00:00
|
|
|
|
def __init__(self, items, endpoint, endpoint_params=None, offset=0, limit=None, **kwargs):
|
2024-05-13 02:52:45 +00:00
|
|
|
|
|
2024-06-29 03:36:26 +00:00
|
|
|
|
super().__init__(**kwargs)
|
2024-05-13 02:52:45 +00:00
|
|
|
|
|
|
|
|
|
self.items_all = items
|
|
|
|
|
self.items_count = len(self.items_all)
|
|
|
|
|
self.offset = offset
|
|
|
|
|
self.limit = limit or app.config['PAGINATION_LIMIT']
|
|
|
|
|
self.page_count = math.ceil(self.items_count / self.limit)
|
|
|
|
|
self.page_current = int(self.offset / self.limit) + 1
|
|
|
|
|
self.page_spread = app.config['PAGINATION_SPREAD']
|
2024-05-23 21:27:21 +00:00
|
|
|
|
if self.page_spread > self.page_count:
|
|
|
|
|
self.page_spread = self.page_count
|
2024-05-13 02:52:45 +00:00
|
|
|
|
|
|
|
|
|
# no menu if its just a single page
|
|
|
|
|
if self.page_count <= 1:
|
|
|
|
|
|
|
|
|
|
self.items = self.items_all
|
|
|
|
|
self.menu = None
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
|
|
if isinstance(items, peewee.Query):
|
|
|
|
|
|
|
|
|
|
# if items are actually a database query, we apply offset and limit
|
|
|
|
|
self.items = self.items_all.offset(self.offset).limit(self.limit)
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
|
|
# otherwise we do the equivalent for a list/iterable
|
|
|
|
|
if self.offset >= self.items_count:
|
|
|
|
|
self.offset = self.items_count -1 # go to last item if offset overshoots
|
|
|
|
|
|
|
|
|
|
if self.items_count > self.offset + self.limit:
|
|
|
|
|
self.items = self.items_all[self.offset:self.offset+self.limit]
|
|
|
|
|
else:
|
|
|
|
|
self.items = self.items_all[self.offset:]
|
|
|
|
|
|
|
|
|
|
page_prev = (self.page_current - 1) or 1
|
|
|
|
|
page_next = self.page_current + 1 if self.page_current < self.page_count else self.page_count
|
|
|
|
|
page_range = None
|
|
|
|
|
page_spread_per_side = math.floor(self.page_spread / 2)
|
|
|
|
|
|
2024-06-03 15:50:44 +00:00
|
|
|
|
if self.page_current <= page_spread_per_side:
|
2024-05-13 02:52:45 +00:00
|
|
|
|
|
|
|
|
|
# range touches start
|
|
|
|
|
page_range = range(1, 1 + self.page_spread)
|
|
|
|
|
|
|
|
|
|
elif self.page_current + page_spread_per_side >= self.page_count:
|
|
|
|
|
|
|
|
|
|
# range touches end
|
|
|
|
|
page_range = range(self.page_count - self.page_spread + 1, self.page_count + 1)
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
|
|
# range in the middle, not touching either end
|
|
|
|
|
page_range = range(
|
|
|
|
|
self.page_current - math.floor(self.page_spread / 2),
|
|
|
|
|
self.page_current + math.ceil(self.page_spread / 2)
|
|
|
|
|
)
|
|
|
|
|
|
2024-05-13 03:35:26 +00:00
|
|
|
|
if not endpoint_params:
|
|
|
|
|
endpoint_params = {}
|
|
|
|
|
|
2024-05-13 02:52:45 +00:00
|
|
|
|
self.menu = Menu()
|
|
|
|
|
|
|
|
|
|
self.menu.append(
|
|
|
|
|
endpoint=endpoint,
|
|
|
|
|
label='⏮',
|
|
|
|
|
params = {
|
2024-05-13 03:35:26 +00:00
|
|
|
|
'offset': 0,
|
|
|
|
|
**endpoint_params,
|
2024-05-13 02:52:45 +00:00
|
|
|
|
},
|
|
|
|
|
classes='first current-page' if self.page_current == 1 else 'first'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.menu.append(
|
|
|
|
|
endpoint=endpoint,
|
|
|
|
|
label='⏴',
|
|
|
|
|
params={
|
|
|
|
|
'offset': (page_prev - 1) * self.limit,
|
2024-05-13 03:35:26 +00:00
|
|
|
|
**endpoint_params,
|
2024-05-13 02:52:45 +00:00
|
|
|
|
},
|
|
|
|
|
classes='previous current-page' if self.page_current == 1 else 'previous'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
for page in page_range:
|
|
|
|
|
|
|
|
|
|
self.menu.append(
|
|
|
|
|
endpoint=endpoint,
|
|
|
|
|
label=page,
|
|
|
|
|
params={
|
|
|
|
|
'offset': (page - 1) * self.limit,
|
2024-05-13 03:35:26 +00:00
|
|
|
|
**endpoint_params,
|
2024-05-13 02:52:45 +00:00
|
|
|
|
},
|
|
|
|
|
classes='current-page' if page == self.page_current else ''
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.menu.append(
|
|
|
|
|
endpoint=endpoint,
|
|
|
|
|
label='⏵',
|
|
|
|
|
params={
|
|
|
|
|
'offset': (page_next - 1) * self.limit,
|
2024-05-13 03:35:26 +00:00
|
|
|
|
**endpoint_params,
|
2024-05-13 02:52:45 +00:00
|
|
|
|
},
|
|
|
|
|
classes='next current-page' if self.page_current == self.page_count else 'next'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.menu.append(
|
|
|
|
|
endpoint=endpoint,
|
|
|
|
|
label='⏭',
|
|
|
|
|
params={
|
2024-05-13 03:35:26 +00:00
|
|
|
|
'offset': (self.page_count - 1) * self.limit,
|
|
|
|
|
**endpoint_params,
|
2024-05-13 02:52:45 +00:00
|
|
|
|
},
|
|
|
|
|
classes='last current-page' if self.page_current == self.page_count else 'last'
|
|
|
|
|
)
|
|
|
|
|
|
2024-04-12 19:22:00 +00:00
|
|
|
|
class Listing(Renderable):
|
|
|
|
|
|
2024-06-29 03:36:26 +00:00
|
|
|
|
def __init__(self, items, offset=0, limit=None, title=None, mode='teaser', menu=None, endpoint=None, endpoint_params=None, item_constructor=None, **kwargs):
|
|
|
|
|
|
|
|
|
|
super().__init__(**kwargs)
|
2024-04-12 19:22:00 +00:00
|
|
|
|
|
|
|
|
|
self.items = items
|
|
|
|
|
self.title = title
|
2024-04-12 19:22:00 +00:00
|
|
|
|
self.mode = mode
|
2024-04-12 19:22:00 +00:00
|
|
|
|
self.menu = menu
|
|
|
|
|
|
2024-05-13 02:52:45 +00:00
|
|
|
|
if endpoint:
|
2024-05-13 03:35:26 +00:00
|
|
|
|
self.pagination = Pagination(items, endpoint, endpoint_params=endpoint_params, offset=offset, limit=limit)
|
2024-05-13 02:52:45 +00:00
|
|
|
|
self.items = self.pagination.items
|
|
|
|
|
else:
|
|
|
|
|
self.pagination = None
|
|
|
|
|
|
|
|
|
|
if item_constructor:
|
|
|
|
|
self.items = [item_constructor(item) for item in self.items]
|
|
|
|
|
|
2024-04-12 19:22:00 +00:00
|
|
|
|
class ErrorPage(Renderable):
|
|
|
|
|
|
2024-06-29 03:36:26 +00:00
|
|
|
|
def __init__(self, message, title='Error', moji=None, status=500, **kwargs):
|
|
|
|
|
|
|
|
|
|
super().__init__(**kwargs)
|
2024-04-12 19:22:00 +00:00
|
|
|
|
|
|
|
|
|
if moji is None:
|
|
|
|
|
if status in app.config['ERROR_CUSTOM_MOJI']:
|
|
|
|
|
moji = app.config['ERROR_CUSTOM_MOJI'][status]
|
|
|
|
|
else:
|
|
|
|
|
moji = '✖'
|
|
|
|
|
|
|
|
|
|
# custom messages from config override messages in code
|
|
|
|
|
if status in app.config['ERROR_CUSTOM_MESSAGES']:
|
|
|
|
|
message = app.config['ERROR_CUSTOM_MESSAGES'][status]
|
|
|
|
|
|
|
|
|
|
self.message = message
|
|
|
|
|
self.title = title
|
|
|
|
|
self.moji = moji
|
|
|
|
|
self.status = status
|