2018-11-29 01:39:20 -06:00
|
|
|
# -*- coding: utf-8; -*-
|
|
|
|
################################################################################
|
|
|
|
#
|
|
|
|
# Rattail -- Retail Software Framework
|
2023-01-12 15:19:46 -06:00
|
|
|
# Copyright © 2010-2023 Lance Edgar
|
2018-11-29 01:39:20 -06:00
|
|
|
#
|
|
|
|
# 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/>.
|
|
|
|
#
|
|
|
|
################################################################################
|
|
|
|
"""
|
|
|
|
App Menus
|
|
|
|
"""
|
|
|
|
|
2022-02-23 00:26:14 -06:00
|
|
|
import re
|
2022-03-05 09:26:25 -06:00
|
|
|
import logging
|
2023-01-14 16:01:26 -06:00
|
|
|
import warnings
|
2021-02-01 11:57:12 -06:00
|
|
|
|
2023-01-14 16:01:26 -06:00
|
|
|
from rattail.app import GenericHandler
|
2022-03-05 09:26:25 -06:00
|
|
|
from rattail.util import import_module_path, prettify, simple_error
|
|
|
|
|
|
|
|
from webhelpers2.html import tags, HTML
|
2022-02-26 17:08:23 -06:00
|
|
|
|
|
|
|
from tailbone.db import Session
|
2018-11-29 01:39:20 -06:00
|
|
|
|
|
|
|
|
2022-03-05 09:26:25 -06:00
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2023-01-14 16:01:26 -06:00
|
|
|
class MenuHandler(GenericHandler):
|
|
|
|
"""
|
|
|
|
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
|
|
|
|
try:
|
|
|
|
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...
|
|
|
|
log.warning("failed to make menus from config", exc_info=True)
|
|
|
|
request.session.flash(simple_error(error), 'error')
|
|
|
|
request.session.flash("Menu config is invalid! Reverting to menus "
|
|
|
|
"defined in code!", 'warning')
|
|
|
|
msg = HTML.literal('Please edit your {} ASAP.'.format(
|
|
|
|
tags.link_to("Menu Config", request.route_url('configure_menus'))))
|
|
|
|
request.session.flash(msg, 'warning')
|
|
|
|
|
|
|
|
# okay, no config, so menus must be built from code..
|
|
|
|
|
|
|
|
# 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 fallback to menu handler method
|
|
|
|
return self.make_menus(request)
|
|
|
|
|
|
|
|
def make_menus_from_config(self, request, **kwargs):
|
|
|
|
"""
|
|
|
|
Try to build a complete menu set from config/settings.
|
|
|
|
|
|
|
|
This will look in the DB settings table, or config file, for
|
|
|
|
menu data. If found, it constructs menus from that data.
|
|
|
|
"""
|
|
|
|
# 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 self.config.getbool('tailbone.menu', 'from_settings',
|
|
|
|
default=False):
|
|
|
|
|
|
|
|
# fetch all menu-related settings at once
|
|
|
|
query = Session().query(model.Setting)\
|
|
|
|
.filter(model.Setting.name.like('tailbone.menu.%'))
|
|
|
|
settings = self.app.cache_model(Session(), model.Setting,
|
|
|
|
query=query, key='name',
|
|
|
|
normalizer=lambda s: s.value)
|
|
|
|
for key in main_keys:
|
|
|
|
menus.append(self.make_single_menu_from_settings(request, key,
|
|
|
|
settings))
|
|
|
|
|
|
|
|
else: # read from config file only
|
|
|
|
for key in main_keys:
|
|
|
|
menus.append(self.make_single_menu_from_config(request, key))
|
|
|
|
|
|
|
|
return menus
|
|
|
|
|
|
|
|
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.
|
|
|
|
"""
|
|
|
|
menu = {
|
|
|
|
'key': key,
|
|
|
|
'type': 'menu',
|
|
|
|
'items': [],
|
|
|
|
}
|
|
|
|
|
|
|
|
# title
|
|
|
|
title = self.config.get('tailbone.menu',
|
|
|
|
'menu.{}.label'.format(key),
|
|
|
|
usedb=False)
|
|
|
|
menu['title'] = title or prettify(key)
|
|
|
|
|
|
|
|
# items
|
|
|
|
item_keys = self.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 = self.config.get('tailbone.menu',
|
|
|
|
'menu.{}.item.{}.label'.format(key, item_key),
|
|
|
|
usedb=False)
|
|
|
|
item['title'] = title or prettify(item_key)
|
|
|
|
|
|
|
|
# route
|
|
|
|
route = self.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 = self.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 = self.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(self, request, key, settings, **kwargs):
|
|
|
|
"""
|
|
|
|
Makes a single top-level menu dict from DB settings.
|
|
|
|
"""
|
|
|
|
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 = self.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_menus(self, request, **kwargs):
|
|
|
|
"""
|
|
|
|
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.
|
|
|
|
"""
|
2023-01-14 18:48:56 -06:00
|
|
|
menus = [
|
2023-01-18 16:55:30 -06:00
|
|
|
self.make_custorders_menu(request),
|
2023-01-14 18:48:56 -06:00
|
|
|
self.make_people_menu(request),
|
|
|
|
self.make_products_menu(request),
|
|
|
|
self.make_vendors_menu(request),
|
2023-01-14 16:01:26 -06:00
|
|
|
]
|
|
|
|
|
2023-01-14 18:48:56 -06:00
|
|
|
integration_menus = self.make_integration_menus(request)
|
|
|
|
if integration_menus:
|
|
|
|
menus.extend(integration_menus)
|
|
|
|
|
|
|
|
menus.extend([
|
2023-01-18 16:55:30 -06:00
|
|
|
self.make_reports_menu(request, include_trainwreck=True),
|
2023-01-14 18:48:56 -06:00
|
|
|
self.make_batches_menu(request),
|
2023-01-18 16:55:30 -06:00
|
|
|
self.make_admin_menu(request, include_stores=True),
|
2023-01-14 18:48:56 -06:00
|
|
|
])
|
|
|
|
|
|
|
|
return menus
|
|
|
|
|
|
|
|
def make_integration_menus(self, request, **kwargs):
|
2023-01-14 16:01:26 -06:00
|
|
|
"""
|
2023-01-14 18:48:56 -06:00
|
|
|
Make a set of menus for all registered system integrations.
|
2023-01-14 16:01:26 -06:00
|
|
|
"""
|
2023-01-14 18:48:56 -06:00
|
|
|
menus = []
|
|
|
|
for provider in self.tb.iter_providers():
|
|
|
|
menu = provider.make_integration_menu(request)
|
|
|
|
if menu:
|
|
|
|
menus.append(menu)
|
|
|
|
menus.sort(key=lambda menu: menu['title'].lower())
|
|
|
|
return menus
|
|
|
|
|
2023-01-18 16:55:30 -06:00
|
|
|
def make_custorders_menu(self, request, **kwargs):
|
|
|
|
"""
|
|
|
|
Generate a typical Customer Orders menu
|
|
|
|
"""
|
|
|
|
return {
|
|
|
|
'title': "Orders",
|
|
|
|
'type': 'menu',
|
|
|
|
'items': [
|
|
|
|
{
|
|
|
|
'title': "New Customer Order",
|
|
|
|
'route': 'custorders.create',
|
|
|
|
'perm': 'custorders.create',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'title': "All New Orders",
|
|
|
|
'route': 'new_custorders',
|
|
|
|
'perm': 'new_custorders.list',
|
|
|
|
},
|
|
|
|
{'type': 'sep'},
|
|
|
|
{
|
|
|
|
'title': "All Customer Orders",
|
|
|
|
'route': 'custorders',
|
|
|
|
'perm': 'custorders.list',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'title': "All Order Items",
|
|
|
|
'route': 'custorders.items',
|
|
|
|
'perm': 'custorders.items.list',
|
|
|
|
},
|
|
|
|
],
|
|
|
|
}
|
|
|
|
|
2023-01-14 18:48:56 -06:00
|
|
|
def make_people_menu(self, request, **kwargs):
|
|
|
|
"""
|
|
|
|
Generate a typical People menu
|
|
|
|
"""
|
|
|
|
return {
|
|
|
|
'title': "People",
|
|
|
|
'type': 'menu',
|
|
|
|
'items': [
|
|
|
|
{
|
|
|
|
'title': "Members",
|
|
|
|
'route': 'members',
|
|
|
|
'perm': 'members.list',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'title': "Customers",
|
|
|
|
'route': 'customers',
|
|
|
|
'perm': 'customers.list',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'title': "Customer Groups",
|
|
|
|
'route': 'customergroups',
|
|
|
|
'perm': 'customergroups.list',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'title': "Employees",
|
|
|
|
'route': 'employees',
|
|
|
|
'perm': 'employees.list',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'title': "All People",
|
|
|
|
'route': 'people',
|
|
|
|
'perm': 'people.list',
|
|
|
|
},
|
2023-01-18 16:55:30 -06:00
|
|
|
{'type': 'sep'},
|
|
|
|
{
|
|
|
|
'title': "Pending Customers",
|
|
|
|
'route': 'pending_customers',
|
|
|
|
'perm': 'pending_customers.list',
|
|
|
|
},
|
2023-01-14 18:48:56 -06:00
|
|
|
],
|
|
|
|
}
|
|
|
|
|
|
|
|
def make_products_menu(self, request, **kwargs):
|
|
|
|
"""
|
|
|
|
Generate a typical Products menu
|
|
|
|
"""
|
|
|
|
return {
|
|
|
|
'title': "Products",
|
|
|
|
'type': 'menu',
|
|
|
|
'items': [
|
|
|
|
{
|
|
|
|
'title': "Products",
|
|
|
|
'route': 'products',
|
|
|
|
'perm': 'products.list',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'title': "Departments",
|
|
|
|
'route': 'departments',
|
|
|
|
'perm': 'departments.list',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'title': "Subdepartments",
|
|
|
|
'route': 'subdepartments',
|
|
|
|
'perm': 'subdepartments.list',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'title': "Brands",
|
|
|
|
'route': 'brands',
|
|
|
|
'perm': 'brands.list',
|
|
|
|
},
|
2023-01-18 19:21:34 -06:00
|
|
|
{
|
|
|
|
'title': "Categories",
|
|
|
|
'route': 'categories',
|
|
|
|
'perm': 'categories.list',
|
|
|
|
},
|
2023-01-14 18:48:56 -06:00
|
|
|
{
|
|
|
|
'title': "Families",
|
|
|
|
'route': 'families',
|
|
|
|
'perm': 'families.list',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'title': "Report Codes",
|
|
|
|
'route': 'reportcodes',
|
|
|
|
'perm': 'reportcodes.list',
|
|
|
|
},
|
2023-01-18 16:55:30 -06:00
|
|
|
{
|
|
|
|
'title': "Units of Measure",
|
|
|
|
'route': 'uoms',
|
|
|
|
'perm': 'uoms.list',
|
|
|
|
},
|
|
|
|
{'type': 'sep'},
|
|
|
|
{
|
|
|
|
'title': "Pending Products",
|
|
|
|
'route': 'pending_products',
|
|
|
|
'perm': 'pending_products.list',
|
|
|
|
},
|
2023-01-14 18:48:56 -06:00
|
|
|
],
|
|
|
|
}
|
|
|
|
|
|
|
|
def make_vendors_menu(self, request, **kwargs):
|
|
|
|
"""
|
|
|
|
Generate a typical Vendors menu
|
|
|
|
"""
|
|
|
|
return {
|
|
|
|
'title': "Vendors",
|
|
|
|
'type': 'menu',
|
|
|
|
'items': [
|
|
|
|
{
|
|
|
|
'title': "Vendors",
|
|
|
|
'route': 'vendors',
|
|
|
|
'perm': 'vendors.list',
|
|
|
|
},
|
|
|
|
{'type': 'sep'},
|
|
|
|
{
|
|
|
|
'title': "Ordering",
|
|
|
|
'route': 'ordering',
|
|
|
|
'perm': 'ordering.list',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'title': "Receiving",
|
|
|
|
'route': 'receiving',
|
|
|
|
'perm': 'receiving.list',
|
|
|
|
},
|
2023-01-18 19:21:34 -06:00
|
|
|
{
|
|
|
|
'title': "Invoice Costing",
|
|
|
|
'route': 'invoice_costing',
|
|
|
|
'perm': 'invoice_costing.list',
|
|
|
|
},
|
2023-01-14 18:48:56 -06:00
|
|
|
{'type': 'sep'},
|
|
|
|
{
|
|
|
|
'title': "Purchases",
|
|
|
|
'route': 'purchases',
|
|
|
|
'perm': 'purchases.list',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'title': "Credits",
|
|
|
|
'route': 'purchases.credits',
|
|
|
|
'perm': 'purchases.credits.list',
|
|
|
|
},
|
2023-01-18 16:55:30 -06:00
|
|
|
{'type': 'sep'},
|
|
|
|
{
|
|
|
|
'title': "Catalog Batches",
|
|
|
|
'route': 'vendorcatalogs',
|
|
|
|
'perm': 'vendorcatalogs.list',
|
|
|
|
},
|
|
|
|
],
|
2023-01-14 18:48:56 -06:00
|
|
|
}
|
2023-01-14 16:01:26 -06:00
|
|
|
|
2023-01-14 18:48:56 -06:00
|
|
|
def make_batches_menu(self, request, **kwargs):
|
|
|
|
"""
|
|
|
|
Generate a typical Batches menu
|
|
|
|
"""
|
|
|
|
return {
|
|
|
|
'title': "Batches",
|
|
|
|
'type': 'menu',
|
|
|
|
'items': [
|
|
|
|
{
|
|
|
|
'title': "Handheld",
|
|
|
|
'route': 'batch.handheld',
|
|
|
|
'perm': 'batch.handheld.list',
|
|
|
|
},
|
|
|
|
{
|
2023-01-18 16:55:30 -06:00
|
|
|
'title': "Inventory",
|
|
|
|
'route': 'batch.inventory',
|
|
|
|
'perm': 'batch.inventory.list',
|
2023-01-14 18:48:56 -06:00
|
|
|
},
|
|
|
|
{
|
2023-01-18 16:55:30 -06:00
|
|
|
'title': "Import / Export",
|
|
|
|
'route': 'batch.importer',
|
|
|
|
'perm': 'batch.importer.list',
|
2023-01-14 18:48:56 -06:00
|
|
|
},
|
2023-01-18 16:55:30 -06:00
|
|
|
],
|
|
|
|
}
|
|
|
|
|
|
|
|
def make_reports_menu(self, request, **kwargs):
|
|
|
|
"""
|
|
|
|
Generate a typical Reports menu
|
|
|
|
"""
|
|
|
|
items = [
|
|
|
|
{
|
|
|
|
'title': "New Report",
|
|
|
|
'route': 'report_output.create',
|
|
|
|
'perm': 'report_output.create',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'title': "Generated Reports",
|
|
|
|
'route': 'report_output',
|
|
|
|
'perm': 'report_output.list',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'title': "Problem Reports",
|
|
|
|
'route': 'problem_reports',
|
|
|
|
'perm': 'problem_reports.list',
|
|
|
|
},
|
|
|
|
]
|
|
|
|
|
|
|
|
if kwargs.get('include_poser', False):
|
|
|
|
items.extend([
|
2023-01-14 18:48:56 -06:00
|
|
|
{'type': 'sep'},
|
|
|
|
{
|
2023-01-18 16:55:30 -06:00
|
|
|
'title': "Poser Reports",
|
|
|
|
'route': 'poser_reports',
|
|
|
|
'perm': 'poser_reports.list',
|
2023-01-14 18:48:56 -06:00
|
|
|
},
|
2023-01-18 16:55:30 -06:00
|
|
|
])
|
|
|
|
|
2023-02-03 16:08:29 -06:00
|
|
|
if kwargs.get('include_worksheets', False):
|
|
|
|
items.extend([
|
|
|
|
{'type': 'sep'},
|
|
|
|
{
|
|
|
|
'title': "Ordering Worksheet",
|
|
|
|
'route': 'reports.ordering',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'title': "Inventory Worksheet",
|
|
|
|
'route': 'reports.inventory',
|
|
|
|
},
|
|
|
|
])
|
|
|
|
|
2023-01-18 16:55:30 -06:00
|
|
|
if kwargs.get('include_trainwreck', False):
|
|
|
|
items.extend([
|
2023-01-14 18:48:56 -06:00
|
|
|
{'type': 'sep'},
|
|
|
|
{
|
2023-01-18 16:55:30 -06:00
|
|
|
'title': "Trainwreck",
|
|
|
|
'route': 'trainwreck.transactions',
|
|
|
|
'perm': 'trainwreck.transactions.list',
|
2023-01-14 18:48:56 -06:00
|
|
|
},
|
2023-01-18 16:55:30 -06:00
|
|
|
])
|
|
|
|
|
|
|
|
return {
|
|
|
|
'title': "Reports",
|
|
|
|
'type': 'menu',
|
|
|
|
'items': items,
|
|
|
|
}
|
|
|
|
|
|
|
|
def make_tempmon_menu(self, request, **kwargs):
|
|
|
|
"""
|
|
|
|
Generate a typical TempMon menu
|
|
|
|
"""
|
|
|
|
return {
|
|
|
|
'title': "TempMon",
|
|
|
|
'type': 'menu',
|
|
|
|
'items': [
|
2023-01-18 21:54:24 -06:00
|
|
|
{
|
|
|
|
'title': "Dashboard",
|
|
|
|
'route': 'tempmon.dashboard',
|
|
|
|
'perm': 'tempmon.appliances.dashboard',
|
|
|
|
},
|
|
|
|
{'type': 'sep'},
|
2023-01-14 18:48:56 -06:00
|
|
|
{
|
2023-01-18 16:55:30 -06:00
|
|
|
'title': "Appliances",
|
|
|
|
'route': 'tempmon.appliances',
|
|
|
|
'perm': 'tempmon.appliances.list',
|
2023-01-14 18:48:56 -06:00
|
|
|
},
|
|
|
|
{
|
2023-01-18 16:55:30 -06:00
|
|
|
'title': "Clients",
|
|
|
|
'route': 'tempmon.clients',
|
|
|
|
'perm': 'tempmon.clients.list',
|
2023-01-14 18:48:56 -06:00
|
|
|
},
|
|
|
|
{
|
2023-01-18 16:55:30 -06:00
|
|
|
'title': "Probes",
|
|
|
|
'route': 'tempmon.probes',
|
|
|
|
'perm': 'tempmon.probes.list',
|
2023-01-14 18:48:56 -06:00
|
|
|
},
|
|
|
|
{
|
2023-01-18 16:55:30 -06:00
|
|
|
'title': "Readings",
|
|
|
|
'route': 'tempmon.readings',
|
|
|
|
'perm': 'tempmon.readings.list',
|
2023-01-14 18:48:56 -06:00
|
|
|
},
|
2023-01-18 16:55:30 -06:00
|
|
|
],
|
|
|
|
}
|
|
|
|
|
|
|
|
def make_admin_menu(self, request, **kwargs):
|
|
|
|
"""
|
|
|
|
Generate a typical Admin menu
|
|
|
|
"""
|
|
|
|
items = []
|
|
|
|
|
|
|
|
if kwargs.get('include_stores', True):
|
|
|
|
items.extend([
|
2023-01-14 18:48:56 -06:00
|
|
|
{
|
2023-01-18 16:55:30 -06:00
|
|
|
'title': "Stores",
|
|
|
|
'route': 'stores',
|
|
|
|
'perm': 'stores.list',
|
2023-01-14 18:48:56 -06:00
|
|
|
},
|
2023-01-18 16:55:30 -06:00
|
|
|
{'type': 'sep'},
|
|
|
|
])
|
|
|
|
|
|
|
|
items.extend([
|
|
|
|
{
|
|
|
|
'title': "Users",
|
|
|
|
'route': 'users',
|
|
|
|
'perm': 'users.list',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'title': "Roles",
|
|
|
|
'route': 'roles',
|
|
|
|
'perm': 'roles.list',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'title': "Raw Permissions",
|
|
|
|
'route': 'permissions',
|
|
|
|
'perm': 'permissions.list',
|
|
|
|
},
|
|
|
|
{'type': 'sep'},
|
|
|
|
{
|
|
|
|
'title': "Email Settings",
|
|
|
|
'route': 'emailprofiles',
|
|
|
|
'perm': 'emailprofiles.list',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'title': "Email Attempts",
|
|
|
|
'route': 'email_attempts',
|
|
|
|
'perm': 'email_attempts.list',
|
|
|
|
},
|
|
|
|
{'type': 'sep'},
|
|
|
|
{
|
|
|
|
'title': "DataSync Status",
|
|
|
|
'route': 'datasync.status',
|
|
|
|
'perm': 'datasync.status',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'title': "DataSync Changes",
|
|
|
|
'route': 'datasyncchanges',
|
|
|
|
'perm': 'datasync_changes.list',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'title': "Importing / Exporting",
|
|
|
|
'route': 'importing',
|
|
|
|
'perm': 'importing.list',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'title': "Luigi Tasks",
|
|
|
|
'route': 'luigi',
|
|
|
|
'perm': 'luigi.list',
|
|
|
|
},
|
|
|
|
{'type': 'sep'},
|
|
|
|
{
|
|
|
|
'title': "App Details",
|
|
|
|
'route': 'appinfo',
|
|
|
|
'perm': 'appinfo.list',
|
|
|
|
},
|
2023-01-18 21:54:24 -06:00
|
|
|
{
|
|
|
|
'title': "Label Settings",
|
|
|
|
'route': 'labelprofiles',
|
|
|
|
'perm': 'labelprofiles.list',
|
|
|
|
},
|
2023-01-18 16:55:30 -06:00
|
|
|
{
|
|
|
|
'title': "Raw Settings",
|
|
|
|
'route': 'settings',
|
|
|
|
'perm': 'settings.list',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'title': "Upgrades",
|
|
|
|
'route': 'upgrades',
|
|
|
|
'perm': 'upgrades.list',
|
|
|
|
},
|
|
|
|
])
|
|
|
|
|
|
|
|
return {
|
|
|
|
'title': "Admin",
|
|
|
|
'type': 'menu',
|
|
|
|
'items': items,
|
2023-01-14 16:01:26 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-11-29 01:39:20 -06:00
|
|
|
def make_simple_menus(request):
|
|
|
|
"""
|
|
|
|
Build the main menu list for the app.
|
|
|
|
"""
|
2023-01-14 16:01:26 -06:00
|
|
|
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)
|
2018-11-29 01:39:20 -06:00
|
|
|
|
2022-02-26 17:08:23 -06:00
|
|
|
# now we have "simple" (raw) menus definition, but must refine
|
|
|
|
# that somewhat to produce our final menus
|
2021-02-01 11:57:12 -06:00
|
|
|
mark_allowed(request, raw_menus)
|
2018-11-29 01:39:20 -06:00
|
|
|
final_menus = []
|
|
|
|
for topitem in raw_menus:
|
|
|
|
|
2021-02-01 11:57:12 -06:00
|
|
|
if topitem['allowed']:
|
2018-11-29 14:00:11 -06:00
|
|
|
|
2021-02-01 11:57:12 -06:00
|
|
|
if topitem.get('type') == 'link':
|
2022-02-26 17:08:23 -06:00
|
|
|
final_menus.append(make_menu_entry(request, topitem))
|
2018-11-29 14:00:11 -06:00
|
|
|
|
2021-02-01 11:57:12 -06:00
|
|
|
else: # assuming 'menu' type
|
2018-11-29 01:39:20 -06:00
|
|
|
|
2018-11-29 14:51:57 -06:00
|
|
|
menu_items = []
|
2021-02-01 11:57:12 -06:00
|
|
|
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']:
|
2022-02-26 17:08:23 -06:00
|
|
|
submenu_items.append(make_menu_entry(request, subitem))
|
2022-02-23 00:26:14 -06:00
|
|
|
menu_items.append({
|
|
|
|
'type': 'submenu',
|
|
|
|
'title': item['title'],
|
|
|
|
'items': submenu_items,
|
|
|
|
'is_menu': True,
|
|
|
|
'is_sep': False,
|
|
|
|
})
|
2021-02-01 11:57:12 -06:00
|
|
|
|
|
|
|
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)
|
2022-02-23 00:26:14 -06:00
|
|
|
if menu_items and not menu_items[-1]['is_sep']:
|
2022-02-26 17:08:23 -06:00
|
|
|
menu_items.append(make_menu_entry(request, item))
|
2018-11-29 14:51:57 -06:00
|
|
|
|
2021-02-01 11:57:12 -06:00
|
|
|
else: # standard menu item
|
2022-02-26 17:08:23 -06:00
|
|
|
menu_items.append(make_menu_entry(request, item))
|
2018-11-29 14:51:57 -06:00
|
|
|
|
|
|
|
# remove final separator if present
|
2022-02-23 00:26:14 -06:00
|
|
|
if menu_items and menu_items[-1]['is_sep']:
|
2018-11-29 14:51:57 -06:00
|
|
|
menu_items.pop()
|
|
|
|
|
|
|
|
# only add if we wound up with something
|
2021-02-01 11:57:12 -06:00
|
|
|
assert menu_items
|
2018-11-29 14:51:57 -06:00
|
|
|
if menu_items:
|
2022-02-23 00:26:14 -06:00
|
|
|
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']:
|
2022-02-26 17:08:23 -06:00
|
|
|
group['key'] = make_menu_key(request.rattail_config,
|
|
|
|
topitem['title'])
|
2022-02-23 00:26:14 -06:00
|
|
|
|
|
|
|
final_menus.append(group)
|
2018-11-29 01:39:20 -06:00
|
|
|
|
|
|
|
return final_menus
|
2021-02-01 11:57:12 -06:00
|
|
|
|
|
|
|
|
2022-02-26 17:08:23 -06:00
|
|
|
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):
|
2021-02-01 11:57:12 -06:00
|
|
|
"""
|
|
|
|
Convert a simple menu entry dict, into a proper menu-related object, for
|
|
|
|
use in constructing final menu.
|
|
|
|
"""
|
|
|
|
# separator
|
|
|
|
if item.get('type') == 'sep':
|
2022-02-23 00:26:14 -06:00
|
|
|
return {
|
|
|
|
'type': 'sep',
|
|
|
|
'is_menu': False,
|
|
|
|
'is_sep': True,
|
|
|
|
}
|
2021-02-01 11:57:12 -06:00
|
|
|
|
|
|
|
# standard menu item
|
2022-02-26 17:08:23 -06:00
|
|
|
entry = {
|
2022-02-23 00:26:14 -06:00
|
|
|
'type': 'item',
|
|
|
|
'title': item['title'],
|
2022-02-26 17:08:23 -06:00
|
|
|
'perm': item.get('perm'),
|
2022-02-23 00:26:14 -06:00
|
|
|
'target': item.get('target'),
|
|
|
|
'is_link': True,
|
|
|
|
'is_menu': False,
|
|
|
|
'is_sep': False,
|
|
|
|
}
|
2022-02-26 17:08:23 -06:00
|
|
|
if item.get('route'):
|
|
|
|
entry['route'] = item['route']
|
2022-12-10 10:09:39 -06:00
|
|
|
try:
|
|
|
|
entry['url'] = request.route_url(entry['route'])
|
|
|
|
except KeyError: # happens if no such route
|
2023-01-14 18:48:56 -06:00
|
|
|
log.debug("invalid route name for menu entry: %s", entry)
|
2022-12-10 10:09:39 -06:00
|
|
|
entry['url'] = entry['route']
|
2022-02-26 17:08:23 -06:00
|
|
|
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
|
2021-02-01 11:57:12 -06:00
|
|
|
|
|
|
|
|
|
|
|
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
|
2023-01-12 15:19:46 -06:00
|
|
|
|
|
|
|
|
2023-01-14 16:01:26 -06:00
|
|
|
def make_admin_menu(request, **kwargs):
|
2023-01-12 15:19:46 -06:00
|
|
|
"""
|
|
|
|
Generate a typical Admin menu
|
|
|
|
"""
|
2023-01-14 16:01:26 -06:00
|
|
|
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)
|