Add new handlers, TailboneHandler and MenuHandler
This commit is contained in:
parent
cfdaa1e927
commit
39d53617bd
49
tailbone/handler.py
Normal file
49
tailbone/handler.py
Normal file
|
@ -0,0 +1,49 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2023 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Tailbone Handler
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals, absolute_import
|
||||
|
||||
from rattail.app import GenericHandler
|
||||
|
||||
|
||||
class TailboneHandler(GenericHandler):
|
||||
"""
|
||||
Base class and default implementation for Tailbone handler.
|
||||
"""
|
||||
|
||||
def get_menu_handler(self, **kwargs):
|
||||
"""
|
||||
Get the configured "menu" handler.
|
||||
|
||||
:returns: The :class:`~tailbone.menus.MenuHandler` instance
|
||||
for the app.
|
||||
"""
|
||||
if not hasattr(self, 'menu_handler'):
|
||||
spec = self.config.get('tailbone.menus', 'handler',
|
||||
default='tailbone.menus:MenuHandler')
|
||||
Handler = self.app.load_object(spec)
|
||||
self.menu_handler = Handler(self.config)
|
||||
return self.menu_handler
|
|
@ -28,7 +28,9 @@ from __future__ import unicode_literals, absolute_import
|
|||
|
||||
import re
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
from rattail.app import GenericHandler
|
||||
from rattail.util import import_module_path, prettify, simple_error
|
||||
|
||||
from webhelpers2.html import tags, HTML
|
||||
|
@ -39,16 +41,26 @@ from tailbone.db import Session
|
|||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def make_simple_menus(request):
|
||||
class MenuHandler(GenericHandler):
|
||||
"""
|
||||
Build the main menu list for the app.
|
||||
Base class and default implementation for menu handler.
|
||||
"""
|
||||
|
||||
def make_raw_menus(self, request, **kwargs):
|
||||
"""
|
||||
Generate a full set of "raw" menus for the app.
|
||||
|
||||
The "raw" menus are basically just a set of dicts to represent
|
||||
the final menus.
|
||||
"""
|
||||
# first try to make menus from config, but this is highly
|
||||
# susceptible to failure, so try to warn user of problems
|
||||
raw_menus = None
|
||||
try:
|
||||
raw_menus = make_menus_from_config(request)
|
||||
menus = self.make_menus_from_config(request)
|
||||
if menus:
|
||||
return menus
|
||||
except Exception as error:
|
||||
|
||||
# TODO: these messages show up multiple times on some pages?!
|
||||
# that must mean the BeforeRender event is firing multiple
|
||||
# times..but why?? seems like there is only 1 request...
|
||||
|
@ -60,141 +72,64 @@ def make_simple_menus(request):
|
|||
tags.link_to("Menu Config", request.route_url('configure_menus'))))
|
||||
request.session.flash(msg, 'warning')
|
||||
|
||||
if not raw_menus:
|
||||
# okay, no config, so menus must be built from code..
|
||||
|
||||
# no config, so import/invoke code 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)
|
||||
# first check for a "simple menus" module; use that if defined
|
||||
menumod = self.config.get('tailbone', 'menus')
|
||||
if menumod:
|
||||
menumod = import_module_path(menumod)
|
||||
if (not hasattr(menumod, 'simple_menus')
|
||||
or not callable(menumod.simple_menus)):
|
||||
raise RuntimeError("module does not have a simple_menus() "
|
||||
"callable: {}".format(menumod))
|
||||
return menumod.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:
|
||||
# now we fallback to menu handler method
|
||||
return self.make_menus(request)
|
||||
|
||||
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_menus_from_config(request):
|
||||
def make_menus_from_config(self, request, **kwargs):
|
||||
"""
|
||||
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.
|
||||
This will look in the DB settings table, or config file, for
|
||||
menu data. If found, it constructs menus from that data.
|
||||
"""
|
||||
config = request.rattail_config
|
||||
main_keys = config.getlist('tailbone.menu', 'menus')
|
||||
# bail unless config defines top-level menu keys
|
||||
main_keys = self.config.getlist('tailbone.menu', 'menus')
|
||||
if not main_keys:
|
||||
return
|
||||
|
||||
model = self.model
|
||||
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',
|
||||
# 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 self.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,
|
||||
settings = self.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))
|
||||
menus.append(self.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))
|
||||
menus.append(self.make_single_menu_from_config(request, key))
|
||||
|
||||
return menus
|
||||
|
||||
|
||||
def make_single_menu_from_config(request, key):
|
||||
def make_single_menu_from_config(self, request, key, **kwargs):
|
||||
"""
|
||||
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.
|
||||
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',
|
||||
|
@ -202,13 +137,13 @@ def make_single_menu_from_config(request, key):
|
|||
}
|
||||
|
||||
# title
|
||||
title = config.get('tailbone.menu',
|
||||
title = self.config.get('tailbone.menu',
|
||||
'menu.{}.label'.format(key),
|
||||
usedb=False)
|
||||
menu['title'] = title or prettify(key)
|
||||
|
||||
# items
|
||||
item_keys = config.getlist('tailbone.menu',
|
||||
item_keys = self.config.getlist('tailbone.menu',
|
||||
'menu.{}.items'.format(key),
|
||||
usedb=False)
|
||||
for item_key in item_keys:
|
||||
|
@ -222,13 +157,13 @@ def make_single_menu_from_config(request, key):
|
|||
item['key'] = item_key
|
||||
|
||||
# title
|
||||
title = config.get('tailbone.menu',
|
||||
title = self.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',
|
||||
route = self.config.get('tailbone.menu',
|
||||
'menu.{}.item.{}.route'.format(key, item_key),
|
||||
usedb=False)
|
||||
if route:
|
||||
|
@ -238,7 +173,7 @@ def make_single_menu_from_config(request, key):
|
|||
else:
|
||||
|
||||
# url
|
||||
url = config.get('tailbone.menu',
|
||||
url = self.config.get('tailbone.menu',
|
||||
'menu.{}.item.{}.url'.format(key, item_key),
|
||||
usedb=False)
|
||||
if not url:
|
||||
|
@ -248,7 +183,7 @@ def make_single_menu_from_config(request, key):
|
|||
item['url'] = url
|
||||
|
||||
# perm
|
||||
perm = config.get('tailbone.menu',
|
||||
perm = self.config.get('tailbone.menu',
|
||||
'menu.{}.item.{}.perm'.format(key, item_key),
|
||||
usedb=False)
|
||||
item['perm'] = perm or '{}.list'.format(item_key)
|
||||
|
@ -257,12 +192,10 @@ def make_single_menu_from_config(request, key):
|
|||
|
||||
return menu
|
||||
|
||||
|
||||
def make_single_menu_from_settings(request, key, settings):
|
||||
def make_single_menu_from_settings(self, request, key, settings, **kwargs):
|
||||
"""
|
||||
Makes a single top-level menu dict from DB settings.
|
||||
"""
|
||||
config = request.rattail_config
|
||||
menu = {
|
||||
'key': key,
|
||||
'type': 'menu',
|
||||
|
@ -274,7 +207,7 @@ def make_single_menu_from_settings(request, key, settings):
|
|||
menu['title'] = title or prettify(key)
|
||||
|
||||
# items
|
||||
item_keys = config.parse_list(
|
||||
item_keys = self.config.parse_list(
|
||||
settings.get('tailbone.menu.menu.{}.items'.format(key)))
|
||||
for item_key in item_keys:
|
||||
item = {}
|
||||
|
@ -318,94 +251,18 @@ def make_single_menu_from_settings(request, key, settings):
|
|||
|
||||
return menu
|
||||
|
||||
|
||||
def make_menu_key(config, value):
|
||||
def make_menus(self, request, **kwargs):
|
||||
"""
|
||||
Generate a normalized menu key for the given value.
|
||||
Make the full set of menus for the app.
|
||||
|
||||
This method provides a semi-sane menu set by default, but it
|
||||
is expected for most apps to override it.
|
||||
"""
|
||||
return re.sub(r'\W', '', value.lower())
|
||||
return [
|
||||
self.make_admin_menu(request),
|
||||
]
|
||||
|
||||
|
||||
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, include_stores=False):
|
||||
def make_admin_menu(self, request, include_stores=False, **kwargs):
|
||||
"""
|
||||
Generate a typical Admin menu
|
||||
"""
|
||||
|
@ -503,3 +360,184 @@ def make_admin_menu(request, include_stores=False):
|
|||
'type': 'menu',
|
||||
'items': items,
|
||||
}
|
||||
|
||||
|
||||
def make_simple_menus(request):
|
||||
"""
|
||||
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)
|
||||
|
||||
# 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:
|
||||
|
||||
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)
|
||||
|
|
Loading…
Reference in a new issue