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:
Lance Edgar 2024-07-14 10:52:32 -05:00
parent 0b4629ea29
commit fd1ec01128
4 changed files with 54 additions and 212 deletions

View file

@ -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')

View file

@ -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
mark_allowed(request, raw_menus)
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) DeprecationWarning, stacklevel=2)
super().__init__(*args, **kwargs)
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)

View file

@ -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)

View file

@ -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([