feat: move core menu logic to wuttaweb
tailbone still defines the default menus, and allows for making dynamic menus from config (which wuttaweb does not). also remove some even older logic for "v1" menu functions
This commit is contained in:
parent
0b4629ea29
commit
fd1ec01128
|
@ -24,6 +24,8 @@
|
||||||
Tailbone Handler
|
Tailbone Handler
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import warnings
|
||||||
|
|
||||||
from mako.lookup import TemplateLookup
|
from mako.lookup import TemplateLookup
|
||||||
|
|
||||||
from rattail.app import GenericHandler
|
from rattail.app import GenericHandler
|
||||||
|
@ -46,11 +48,14 @@ class TailboneHandler(GenericHandler):
|
||||||
|
|
||||||
def get_menu_handler(self, **kwargs):
|
def get_menu_handler(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Get the configured "menu" handler.
|
DEPRECATED; use
|
||||||
|
:meth:`wuttaweb.handler.WebHandler.get_menu_handler()`
|
||||||
:returns: The :class:`~tailbone.menus.MenuHandler` instance
|
instead.
|
||||||
for the app.
|
|
||||||
"""
|
"""
|
||||||
|
warnings.warn("TailboneHandler.get_menu_handler() is deprecated; "
|
||||||
|
"please use WebHandler.get_menu_handler() instead",
|
||||||
|
DeprecationWarning, stacklevel=2)
|
||||||
|
|
||||||
if not hasattr(self, 'menu_handler'):
|
if not hasattr(self, 'menu_handler'):
|
||||||
spec = self.config.get('tailbone.menus', 'handler',
|
spec = self.config.get('tailbone.menus', 'handler',
|
||||||
default='tailbone.menus:MenuHandler')
|
default='tailbone.menus:MenuHandler')
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2023 Lance Edgar
|
# Copyright © 2010-2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -24,37 +24,48 @@
|
||||||
App Menus
|
App Menus
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
|
||||||
import logging
|
import logging
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from rattail.app import GenericHandler
|
|
||||||
from rattail.util import prettify, simple_error
|
from rattail.util import prettify, simple_error
|
||||||
|
|
||||||
from webhelpers2.html import tags, HTML
|
from webhelpers2.html import tags, HTML
|
||||||
|
|
||||||
|
from wuttaweb.menus import MenuHandler as WuttaMenuHandler
|
||||||
|
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class MenuHandler(GenericHandler):
|
class TailboneMenuHandler(WuttaMenuHandler):
|
||||||
"""
|
"""
|
||||||
Base class and default implementation for menu handler.
|
Base class and default implementation for menu handler.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def make_raw_menus(self, request, **kwargs):
|
##############################
|
||||||
"""
|
# internal methods
|
||||||
Generate a full set of "raw" menus for the app.
|
##############################
|
||||||
|
|
||||||
The "raw" menus are basically just a set of dicts to represent
|
def _is_allowed(self, request, item):
|
||||||
the final menus.
|
"""
|
||||||
|
TODO: must override this until wuttaweb has proper user auth checks
|
||||||
|
"""
|
||||||
|
perm = item.get('perm')
|
||||||
|
if perm:
|
||||||
|
return request.has_perm(perm)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _make_raw_menus(self, request, **kwargs):
|
||||||
|
"""
|
||||||
|
We are overriding this to allow for making dynamic menus from
|
||||||
|
config/settings. Which may or may not be a good idea..
|
||||||
"""
|
"""
|
||||||
# first try to make menus from config, but this is highly
|
# first try to make menus from config, but this is highly
|
||||||
# susceptible to failure, so try to warn user of problems
|
# susceptible to failure, so try to warn user of problems
|
||||||
try:
|
try:
|
||||||
menus = self.make_menus_from_config(request)
|
menus = self._make_menus_from_config(request)
|
||||||
if menus:
|
if menus:
|
||||||
return menus
|
return menus
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
|
@ -71,9 +82,9 @@ class MenuHandler(GenericHandler):
|
||||||
request.session.flash(msg, 'warning')
|
request.session.flash(msg, 'warning')
|
||||||
|
|
||||||
# okay, no config, so menus will be built from code
|
# okay, no config, so menus will be built from code
|
||||||
return self.make_menus(request)
|
return self.make_menus(request, **kwargs)
|
||||||
|
|
||||||
def make_menus_from_config(self, request, **kwargs):
|
def _make_menus_from_config(self, request, **kwargs):
|
||||||
"""
|
"""
|
||||||
Try to build a complete menu set from config/settings.
|
Try to build a complete menu set from config/settings.
|
||||||
|
|
||||||
|
@ -101,16 +112,15 @@ class MenuHandler(GenericHandler):
|
||||||
query=query, key='name',
|
query=query, key='name',
|
||||||
normalizer=lambda s: s.value)
|
normalizer=lambda s: s.value)
|
||||||
for key in main_keys:
|
for key in main_keys:
|
||||||
menus.append(self.make_single_menu_from_settings(request, key,
|
menus.append(self._make_single_menu_from_settings(request, key, settings))
|
||||||
settings))
|
|
||||||
|
|
||||||
else: # read from config file only
|
else: # read from config file only
|
||||||
for key in main_keys:
|
for key in main_keys:
|
||||||
menus.append(self.make_single_menu_from_config(request, key))
|
menus.append(self._make_single_menu_from_config(request, key))
|
||||||
|
|
||||||
return menus
|
return menus
|
||||||
|
|
||||||
def make_single_menu_from_config(self, request, key, **kwargs):
|
def _make_single_menu_from_config(self, request, key, **kwargs):
|
||||||
"""
|
"""
|
||||||
Makes a single top-level menu dict from config file. Note
|
Makes a single top-level menu dict from config file. Note
|
||||||
that this will read from config file(s) *only* and avoids
|
that this will read from config file(s) *only* and avoids
|
||||||
|
@ -178,7 +188,7 @@ class MenuHandler(GenericHandler):
|
||||||
|
|
||||||
return menu
|
return menu
|
||||||
|
|
||||||
def make_single_menu_from_settings(self, request, key, settings, **kwargs):
|
def _make_single_menu_from_settings(self, request, key, settings, **kwargs):
|
||||||
"""
|
"""
|
||||||
Makes a single top-level menu dict from DB settings.
|
Makes a single top-level menu dict from DB settings.
|
||||||
"""
|
"""
|
||||||
|
@ -237,6 +247,10 @@ class MenuHandler(GenericHandler):
|
||||||
|
|
||||||
return menu
|
return menu
|
||||||
|
|
||||||
|
##############################
|
||||||
|
# menu defaults
|
||||||
|
##############################
|
||||||
|
|
||||||
def make_menus(self, request, **kwargs):
|
def make_menus(self, request, **kwargs):
|
||||||
"""
|
"""
|
||||||
Make the full set of menus for the app.
|
Make the full set of menus for the app.
|
||||||
|
@ -723,182 +737,10 @@ class MenuHandler(GenericHandler):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def make_simple_menus(request):
|
class MenuHandler(TailboneMenuHandler):
|
||||||
"""
|
|
||||||
Build the main menu list for the app.
|
|
||||||
"""
|
|
||||||
app = request.rattail_config.get_app()
|
|
||||||
tailbone_handler = app.get_tailbone_handler()
|
|
||||||
menu_handler = tailbone_handler.get_menu_handler()
|
|
||||||
|
|
||||||
raw_menus = menu_handler.make_raw_menus(request)
|
def __init__(self, *args, **kwargs):
|
||||||
|
warnings.warn("tailbone.menus.MenuHandler is deprecated; "
|
||||||
# now we have "simple" (raw) menus definition, but must refine
|
"please use tailbone.menus.TailboneMenuHandler instead",
|
||||||
# that somewhat to produce our final menus
|
DeprecationWarning, stacklevel=2)
|
||||||
mark_allowed(request, raw_menus)
|
super().__init__(*args, **kwargs)
|
||||||
final_menus = []
|
|
||||||
for topitem in raw_menus:
|
|
||||||
|
|
||||||
if topitem['allowed']:
|
|
||||||
|
|
||||||
if topitem.get('type') == 'link':
|
|
||||||
final_menus.append(make_menu_entry(request, topitem))
|
|
||||||
|
|
||||||
else: # assuming 'menu' type
|
|
||||||
|
|
||||||
menu_items = []
|
|
||||||
for item in topitem['items']:
|
|
||||||
if not item['allowed']:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# nested submenu
|
|
||||||
if item.get('type') == 'menu':
|
|
||||||
submenu_items = []
|
|
||||||
for subitem in item['items']:
|
|
||||||
if subitem['allowed']:
|
|
||||||
submenu_items.append(make_menu_entry(request, subitem))
|
|
||||||
menu_items.append({
|
|
||||||
'type': 'submenu',
|
|
||||||
'title': item['title'],
|
|
||||||
'items': submenu_items,
|
|
||||||
'is_menu': True,
|
|
||||||
'is_sep': False,
|
|
||||||
})
|
|
||||||
|
|
||||||
elif item.get('type') == 'sep':
|
|
||||||
# we only want to add a sep, *if* we already have some
|
|
||||||
# menu items (i.e. there is something to separate)
|
|
||||||
# *and* the last menu item is not a sep (avoid doubles)
|
|
||||||
if menu_items and not menu_items[-1]['is_sep']:
|
|
||||||
menu_items.append(make_menu_entry(request, item))
|
|
||||||
|
|
||||||
else: # standard menu item
|
|
||||||
menu_items.append(make_menu_entry(request, item))
|
|
||||||
|
|
||||||
# remove final separator if present
|
|
||||||
if menu_items and menu_items[-1]['is_sep']:
|
|
||||||
menu_items.pop()
|
|
||||||
|
|
||||||
# only add if we wound up with something
|
|
||||||
assert menu_items
|
|
||||||
if menu_items:
|
|
||||||
group = {
|
|
||||||
'type': 'menu',
|
|
||||||
'key': topitem.get('key'),
|
|
||||||
'title': topitem['title'],
|
|
||||||
'items': menu_items,
|
|
||||||
'is_menu': True,
|
|
||||||
'is_link': False,
|
|
||||||
}
|
|
||||||
|
|
||||||
# topitem w/ no key likely means it did not come
|
|
||||||
# from config but rather explicit definition in
|
|
||||||
# code. so we are free to "invent" a (safe) key
|
|
||||||
# for it, since that is only for editing config
|
|
||||||
if not group['key']:
|
|
||||||
group['key'] = make_menu_key(request.rattail_config,
|
|
||||||
topitem['title'])
|
|
||||||
|
|
||||||
final_menus.append(group)
|
|
||||||
|
|
||||||
return final_menus
|
|
||||||
|
|
||||||
|
|
||||||
def make_menu_key(config, value):
|
|
||||||
"""
|
|
||||||
Generate a normalized menu key for the given value.
|
|
||||||
"""
|
|
||||||
return re.sub(r'\W', '', value.lower())
|
|
||||||
|
|
||||||
|
|
||||||
def make_menu_entry(request, item):
|
|
||||||
"""
|
|
||||||
Convert a simple menu entry dict, into a proper menu-related object, for
|
|
||||||
use in constructing final menu.
|
|
||||||
"""
|
|
||||||
# separator
|
|
||||||
if item.get('type') == 'sep':
|
|
||||||
return {
|
|
||||||
'type': 'sep',
|
|
||||||
'is_menu': False,
|
|
||||||
'is_sep': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
# standard menu item
|
|
||||||
entry = {
|
|
||||||
'type': 'item',
|
|
||||||
'title': item['title'],
|
|
||||||
'perm': item.get('perm'),
|
|
||||||
'target': item.get('target'),
|
|
||||||
'is_link': True,
|
|
||||||
'is_menu': False,
|
|
||||||
'is_sep': False,
|
|
||||||
}
|
|
||||||
if item.get('route'):
|
|
||||||
entry['route'] = item['route']
|
|
||||||
try:
|
|
||||||
entry['url'] = request.route_url(entry['route'])
|
|
||||||
except KeyError: # happens if no such route
|
|
||||||
log.warning("invalid route name for menu entry: %s", entry)
|
|
||||||
entry['url'] = entry['route']
|
|
||||||
entry['key'] = entry['route']
|
|
||||||
else:
|
|
||||||
if item.get('url'):
|
|
||||||
entry['url'] = item['url']
|
|
||||||
entry['key'] = make_menu_key(request.rattail_config, entry['title'])
|
|
||||||
return entry
|
|
||||||
|
|
||||||
|
|
||||||
def is_allowed(request, item):
|
|
||||||
"""
|
|
||||||
Logic to determine if a given menu item is "allowed" for current user.
|
|
||||||
"""
|
|
||||||
perm = item.get('perm')
|
|
||||||
if perm:
|
|
||||||
return request.has_perm(perm)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def mark_allowed(request, menus):
|
|
||||||
"""
|
|
||||||
Traverse the menu set, and mark each item as "allowed" (or not) based on
|
|
||||||
current user permissions.
|
|
||||||
"""
|
|
||||||
for topitem in menus:
|
|
||||||
|
|
||||||
if topitem.get('type', 'menu') == 'menu':
|
|
||||||
topitem['allowed'] = False
|
|
||||||
|
|
||||||
for item in topitem['items']:
|
|
||||||
|
|
||||||
if item.get('type') == 'menu':
|
|
||||||
for subitem in item['items']:
|
|
||||||
subitem['allowed'] = is_allowed(request, subitem)
|
|
||||||
|
|
||||||
item['allowed'] = False
|
|
||||||
for subitem in item['items']:
|
|
||||||
if subitem['allowed'] and subitem.get('type') != 'sep':
|
|
||||||
item['allowed'] = True
|
|
||||||
break
|
|
||||||
|
|
||||||
else:
|
|
||||||
item['allowed'] = is_allowed(request, item)
|
|
||||||
|
|
||||||
for item in topitem['items']:
|
|
||||||
if item['allowed'] and item.get('type') != 'sep':
|
|
||||||
topitem['allowed'] = True
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
def make_admin_menu(request, **kwargs):
|
|
||||||
"""
|
|
||||||
Generate a typical Admin menu
|
|
||||||
"""
|
|
||||||
warnings.warn("make_admin_menu() function is deprecated; please use "
|
|
||||||
"MenuHandler.make_admin_menu() instead",
|
|
||||||
DeprecationWarning, stacklevel=2)
|
|
||||||
|
|
||||||
app = request.rattail_config.get_app()
|
|
||||||
tailbone_handler = app.get_tailbone_handler()
|
|
||||||
menu_handler = tailbone_handler.get_menu_handler()
|
|
||||||
return menu_handler.make_admin_menu(request, **kwargs)
|
|
||||||
|
|
|
@ -42,7 +42,6 @@ import tailbone
|
||||||
from tailbone import helpers
|
from tailbone import helpers
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.config import csrf_header_name, should_expose_websockets
|
from tailbone.config import csrf_header_name, should_expose_websockets
|
||||||
from tailbone.menus import make_simple_menus
|
|
||||||
from tailbone.util import get_available_themes, get_global_search_options
|
from tailbone.util import get_available_themes, get_global_search_options
|
||||||
|
|
||||||
|
|
||||||
|
@ -180,7 +179,7 @@ def before_render(event):
|
||||||
|
|
||||||
renderer_globals = event
|
renderer_globals = event
|
||||||
|
|
||||||
# wuttaweb overrides
|
# overrides
|
||||||
renderer_globals['h'] = helpers
|
renderer_globals['h'] = helpers
|
||||||
|
|
||||||
# misc.
|
# misc.
|
||||||
|
@ -215,13 +214,6 @@ def before_render(event):
|
||||||
options = [tags.Option(theme, value=theme) for theme in available]
|
options = [tags.Option(theme, value=theme) for theme in available]
|
||||||
renderer_globals['theme_picker_options'] = options
|
renderer_globals['theme_picker_options'] = options
|
||||||
|
|
||||||
# heck while we're assuming the classic web app here...
|
|
||||||
# (we don't want this to happen for the API either!)
|
|
||||||
# TODO: just..awful *shrug*
|
|
||||||
# note that we assume "simple" menus nowadays
|
|
||||||
if config.get_bool('tailbone.menus.simple', default=True):
|
|
||||||
renderer_globals['menus'] = make_simple_menus(request)
|
|
||||||
|
|
||||||
# TODO: ugh, same deal here
|
# TODO: ugh, same deal here
|
||||||
renderer_globals['messaging_enabled'] = config.get_bool('tailbone.messaging.enabled',
|
renderer_globals['messaging_enabled'] = config.get_bool('tailbone.messaging.enabled',
|
||||||
default=False)
|
default=False)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2023 Lance Edgar
|
# Copyright © 2010-2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -30,7 +30,6 @@ import sqlalchemy as sa
|
||||||
|
|
||||||
from tailbone.views import View
|
from tailbone.views import View
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.menus import make_menu_key
|
|
||||||
|
|
||||||
|
|
||||||
class MenuConfigView(View):
|
class MenuConfigView(View):
|
||||||
|
@ -79,12 +78,16 @@ class MenuConfigView(View):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def configure_gather_settings(self, data):
|
def configure_gather_settings(self, data):
|
||||||
|
app = self.get_rattail_app()
|
||||||
|
web = app.get_web_handler()
|
||||||
|
menus = web.get_menu_handler()
|
||||||
|
|
||||||
settings = [{'name': 'tailbone.menu.from_settings',
|
settings = [{'name': 'tailbone.menu.from_settings',
|
||||||
'value': 'true'}]
|
'value': 'true'}]
|
||||||
|
|
||||||
main_keys = []
|
main_keys = []
|
||||||
for topitem in json.loads(data['menus']):
|
for topitem in json.loads(data['menus']):
|
||||||
key = make_menu_key(self.rattail_config, topitem['title'])
|
key = menus._make_menu_key(self.rattail_config, topitem['title'])
|
||||||
main_keys.append(key)
|
main_keys.append(key)
|
||||||
|
|
||||||
settings.extend([
|
settings.extend([
|
||||||
|
@ -99,7 +102,7 @@ class MenuConfigView(View):
|
||||||
if item.get('route'):
|
if item.get('route'):
|
||||||
item_key = item['route']
|
item_key = item['route']
|
||||||
else:
|
else:
|
||||||
item_key = make_menu_key(self.rattail_config, item['title'])
|
item_key = menus._make_menu_key(self.rattail_config, item['title'])
|
||||||
item_keys.append(item_key)
|
item_keys.append(item_key)
|
||||||
|
|
||||||
settings.extend([
|
settings.extend([
|
||||||
|
|
Loading…
Reference in a new issue