Add page/way to configure main menus

just the basics so far, index page routes and separators should be
supported but nothing else.  also "menus from config" is all or
nothing, no way to mix config + code at this point
This commit is contained in:
Lance Edgar 2022-02-26 17:08:23 -06:00
parent 587a4daf7a
commit 74fecf553e
7 changed files with 883 additions and 25 deletions

View file

@ -28,22 +28,28 @@ from __future__ import unicode_literals, absolute_import
import re
from rattail.util import import_module_path
from rattail.util import import_module_path, prettify
from tailbone.db import Session
def make_simple_menus(request):
"""
Build the main menu list for the app.
"""
menus_module = import_module_path(
request.rattail_config.require('tailbone', 'menus'))
# first try to make menus from config
raw_menus = make_menus_from_config(request)
if not raw_menus:
if not hasattr(menus_module, 'simple_menus') or not callable(menus_module.simple_menus):
raise RuntimeError("module does not have a simple_menus() callable: {}".format(menus_module))
# no config, so import/invoke function to build them
menus_module = import_module_path(
request.rattail_config.require('tailbone', 'menus'))
if not hasattr(menus_module, 'simple_menus') or not callable(menus_module.simple_menus):
raise RuntimeError("module does not have a simple_menus() callable: {}".format(menus_module))
raw_menus = menus_module.simple_menus(request)
# collect "simple" menus definition, but must refine that somewhat to
# produce our final menus
raw_menus = menus_module.simple_menus(request)
# now we have "simple" (raw) menus definition, but must refine
# that somewhat to produce our final menus
mark_allowed(request, raw_menus)
final_menus = []
for topitem in raw_menus:
@ -51,7 +57,7 @@ def make_simple_menus(request):
if topitem['allowed']:
if topitem.get('type') == 'link':
final_menus.append(make_menu_entry(topitem))
final_menus.append(make_menu_entry(request, topitem))
else: # assuming 'menu' type
@ -65,7 +71,7 @@ def make_simple_menus(request):
submenu_items = []
for subitem in item['items']:
if subitem['allowed']:
submenu_items.append(make_menu_entry(subitem))
submenu_items.append(make_menu_entry(request, subitem))
menu_items.append({
'type': 'submenu',
'title': item['title'],
@ -79,10 +85,10 @@ def make_simple_menus(request):
# 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(item))
menu_items.append(make_menu_entry(request, item))
else: # standard menu item
menu_items.append(make_menu_entry(item))
menu_items.append(make_menu_entry(request, item))
# remove final separator if present
if menu_items and menu_items[-1]['is_sep']:
@ -105,14 +111,201 @@ def make_simple_menus(request):
# 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'] = re.sub(r'\W', '', topitem['title'].lower())
group['key'] = make_menu_key(request.rattail_config,
topitem['title'])
final_menus.append(group)
return final_menus
def make_menu_entry(item):
def make_menus_from_config(request):
"""
Try to build a complete menu set from config/settings.
This essentially checks for the top-level menu list in config; if
found then it will build a full menu set from config. If this
top-level list is not present in config then menus will be built
purely from code instead. An example of this top-level list:
.. code-hightlight:: ini
[tailbone.menu]
menus = first, second, third, admin
Obviously much more config would be needed to define those menus
etc. but that is the option that determines whether the rest of
menu config is even read, or not.
"""
config = request.rattail_config
main_keys = config.getlist('tailbone.menu', 'menus')
if not main_keys:
return
menus = []
# menu definition can come either from config file or db settings,
# but if the latter then we want to optimize with one big query
if config.getbool('tailbone.menu', 'from_settings',
default=False):
app = config.get_app()
model = config.get_model()
# fetch all menu-related settings at once
query = Session().query(model.Setting)\
.filter(model.Setting.name.like('tailbone.menu.%'))
settings = app.cache_model(Session(), model.Setting,
query=query, key='name',
normalizer=lambda s: s.value)
for key in main_keys:
menus.append(make_single_menu_from_settings(request, key, settings))
else: # read from config file only
for key in main_keys:
menus.append(make_single_menu_from_config(request, key))
return menus
def make_single_menu_from_config(request, key):
"""
Makes a single top-level menu dict from config file. Note that
this will read from config file(s) *only* and avoids querying the
database, for efficiency.
"""
config = request.rattail_config
menu = {
'key': key,
'type': 'menu',
'items': [],
}
# title
title = config.get('tailbone.menu',
'menu.{}.label'.format(key),
usedb=False)
menu['title'] = title or prettify(key)
# items
item_keys = config.getlist('tailbone.menu',
'menu.{}.items'.format(key),
usedb=False)
for item_key in item_keys:
item = {}
if item_key == 'SEP':
item['type'] = 'sep'
else:
item['type'] = 'item'
item['key'] = item_key
# title
title = config.get('tailbone.menu',
'menu.{}.item.{}.label'.format(key, item_key),
usedb=False)
item['title'] = title or prettify(item_key)
# route
route = config.get('tailbone.menu',
'menu.{}.item.{}.route'.format(key, item_key),
usedb=False)
if route:
item['route'] = route
item['url'] = request.route_url(route)
else:
# url
url = config.get('tailbone.menu',
'menu.{}.item.{}.url'.format(key, item_key),
usedb=False)
if not url:
url = request.route_url(item_key)
elif url.startswith('route:'):
url = request.route_url(url[6:])
item['url'] = url
# perm
perm = config.get('tailbone.menu',
'menu.{}.item.{}.perm'.format(key, item_key),
usedb=False)
item['perm'] = perm or '{}.list'.format(item_key)
menu['items'].append(item)
return menu
def make_single_menu_from_settings(request, key, settings):
"""
Makes a single top-level menu dict from DB settings.
"""
config = request.rattail_config
menu = {
'key': key,
'type': 'menu',
'items': [],
}
# title
title = settings.get('tailbone.menu.menu.{}.label'.format(key))
menu['title'] = title or prettify(key)
# items
item_keys = config.parse_list(
settings.get('tailbone.menu.menu.{}.items'.format(key)))
for item_key in item_keys:
item = {}
if item_key == 'SEP':
item['type'] = 'sep'
else:
item['type'] = 'item'
item['key'] = item_key
# title
title = settings.get('tailbone.menu.menu.{}.item.{}.label'.format(
key, item_key))
item['title'] = title or prettify(item_key)
# route
route = settings.get('tailbone.menu.menu.{}.item.{}.route'.format(
key, item_key))
if route:
item['route'] = route
item['url'] = request.route_url(route)
else:
# url
url = settings.get('tailbone.menu.menu.{}.item.{}.url'.format(
key, item_key))
if not url:
url = request.route_url(item_key)
if url.startswith('route:'):
url = request.route_url(url[6:])
item['url'] = url
# perm
perm = settings.get('tailbone.menu.menu.{}.item.{}.perm'.format(
key, item_key))
item['perm'] = perm or '{}.list'.format(item_key)
menu['items'].append(item)
return menu
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.
@ -126,15 +319,24 @@ def make_menu_entry(item):
}
# standard menu item
return {
entry = {
'type': 'item',
'title': item['title'],
'url': item['url'],
'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']
entry['url'] = request.route_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):