diff --git a/tailbone/menus.py b/tailbone/menus.py
index 464f081c..46f5c62a 100644
--- a/tailbone/menus.py
+++ b/tailbone/menus.py
@@ -27,21 +27,42 @@ App Menus
from __future__ import unicode_literals, absolute_import
import re
+import logging
-from rattail.util import import_module_path, prettify
+from rattail.util import import_module_path, prettify, simple_error
+
+from webhelpers2.html import tags, HTML
from tailbone.db import Session
+log = logging.getLogger(__name__)
+
+
def make_simple_menus(request):
"""
Build the main menu list for the app.
"""
- # first try to make menus from config
- raw_menus = make_menus_from_config(request)
+ # 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)
+ 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')
+
if not raw_menus:
- # no config, so import/invoke function to build them
+ # 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):
diff --git a/tailbone/templates/poser/views/configure.mako b/tailbone/templates/poser/views/configure.mako
new file mode 100644
index 00000000..448c56f0
--- /dev/null
+++ b/tailbone/templates/poser/views/configure.mako
@@ -0,0 +1,41 @@
+## -*- coding: utf-8; -*-
+<%inherit file="/configure.mako" />
+
+<%def name="form_content()">
+
+
+ NB. Any changes made here will require an app restart!
+
+
+ Tailbone Views
+
+ People
+ % for key, label in view_settings['people']:
+ ${self.simple_flag(key, label)}
+ % endfor
+
+ Products
+ % for key, label in view_settings['products']:
+ ${self.simple_flag(key, label)}
+ % endfor
+
+ Other
+ % for key, label in view_settings['other']:
+ ${self.simple_flag(key, label)}
+ % endfor
+
+%def>
+
+<%def name="simple_flag(key, label)">
+
+
+ (disabled)
+ ${key}
+
+
+%def>
+
+
+${parent.body()}
diff --git a/tailbone/util.py b/tailbone/util.py
index c0ab4e3e..25e2c587 100644
--- a/tailbone/util.py
+++ b/tailbone/util.py
@@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
-# Copyright © 2010-2021 Lance Edgar
+# Copyright © 2010-2022 Lance Edgar
#
# This file is part of Rattail.
#
@@ -251,3 +251,24 @@ def route_exists(request, route_name):
mapper = reg.getUtility(IRoutesMapper)
route = mapper.get_route(route_name)
return bool(route)
+
+
+def include_configured_views(pyramid_config):
+ """
+ Include arbitrary additional views based on DB settings.
+ """
+ rattail_config = pyramid_config.registry.settings.get('rattail_config')
+ app = rattail_config.get_app()
+ model = rattail_config.get_model()
+ session = app.make_session()
+
+ # fetch all include-related settings at once
+ settings = session.query(model.Setting)\
+ .filter(model.Setting.name.like('tailbone.includes.%'))\
+ .all()
+
+ for setting in settings:
+ if setting.value:
+ pyramid_config.include(setting.value)
+
+ session.close()
diff --git a/tailbone/views/master.py b/tailbone/views/master.py
index 1214d8aa..a42cca8e 100644
--- a/tailbone/views/master.py
+++ b/tailbone/views/master.py
@@ -4245,12 +4245,15 @@ class MasterView(View):
settings = self.configure_gather_settings(data)
self.configure_remove_settings()
self.configure_save_settings(settings)
- self.request.session.flash("Settings have been saved.")
+ self.configure_flash_settings_saved()
return self.redirect(self.request.current_route_url())
context = self.configure_get_context()
return self.render_to_response('configure', context)
+ def configure_flash_settings_saved(self):
+ self.request.session.flash("Settings have been saved.")
+
def configure_process_uploads(self, uploads, data):
if self.has_input_file_templates:
templatesdir = os.path.join(self.rattail_config.datadir(),
diff --git a/tailbone/views/poser/__init__.py b/tailbone/views/poser/__init__.py
index e721c862..b81580d4 100644
--- a/tailbone/views/poser/__init__.py
+++ b/tailbone/views/poser/__init__.py
@@ -29,3 +29,4 @@ from __future__ import unicode_literals, absolute_import
def includeme(config):
config.include('tailbone.views.poser.reports')
+ config.include('tailbone.views.poser.views')
diff --git a/tailbone/views/poser/views.py b/tailbone/views/poser/views.py
new file mode 100644
index 00000000..66a0b0db
--- /dev/null
+++ b/tailbone/views/poser/views.py
@@ -0,0 +1,241 @@
+# -*- coding: utf-8; -*-
+################################################################################
+#
+# Rattail -- Retail Software Framework
+# Copyright © 2010-2022 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 .
+#
+################################################################################
+"""
+Poser Views for Views...
+"""
+
+from __future__ import unicode_literals, absolute_import
+
+import six
+
+import colander
+
+from .master import PoserMasterView
+
+
+class PoserViewView(PoserMasterView):
+ """
+ Master view for Poser views
+ """
+ normalized_model_name = 'poser_view'
+ model_title = "Poser View"
+ route_prefix = 'poser_views'
+ url_prefix = '/poser/views'
+ configurable = True
+ config_title = "Included Views"
+
+ # TODO
+ creatable = False
+ editable = False
+ deletable = False
+ # downloadable = True
+
+ grid_columns = [
+ 'key',
+ 'class_name',
+ 'description',
+ 'error',
+ ]
+
+ def get_poser_data(self, session=None):
+ return self.poser_handler.get_all_tailbone_views()
+
+ def make_form_schema(self):
+ return PoserViewSchema()
+
+ def make_create_form(self):
+ return self.make_form({})
+
+ def configure_form(self, f):
+ super(PoserViewView, self).configure_form(f)
+ view = f.model_instance
+
+ # key
+ f.set_default('key', 'cool_widget')
+ f.set_helptext('key', "Unique key for the view; used as basis for filename.")
+ if self.creating:
+ f.set_validator('view_key', self.unique_view_key)
+
+ # class_name
+ f.set_default('class_name', "CoolWidget")
+ f.set_helptext('class_name', "Python-friendly basis for view class name.")
+
+ # description
+ f.set_default('description', "Master view for Cool Widgets")
+ f.set_helptext('description', "Brief description of the view.")
+
+ def unique_view_key(self, node, value):
+ for view in self.get_data():
+ if view['key'] == value:
+ raise node.raise_invalid("Poser view key must be unique")
+
+ def collect_available_view_settings(self):
+
+ # TODO: this probably should be more dynamic? definitely need
+ # to let integration packages register some more options...
+
+ return {
+
+ 'people': {
+
+ # TODO: need some way for integration / extension
+ # packages to register alternate view options for some
+ # of these. that is the main reason these are dicts
+ # even though at the moment it's a bit overkill.
+
+ 'tailbone.views.customers': {
+ # 'spec': 'tailbone.views.customers',
+ 'label': "Customers",
+ },
+ 'tailbone.views.customergroups': {
+ # 'spec': 'tailbone.views.customergroups',
+ 'label': "Customer Groups",
+ },
+ 'tailbone.views.employees': {
+ # 'spec': 'tailbone.views.employees',
+ 'label': "Employees",
+ },
+ 'tailbone.views.members': {
+ # 'spec': 'tailbone.views.members',
+ 'label': "Members",
+ },
+ },
+
+ 'products': {
+
+ 'tailbone.views.departments': {
+ # 'spec': 'tailbone.views.departments',
+ 'label': "Departments",
+ },
+
+ 'tailbone.views.subdepartments': {
+ # 'spec': 'tailbone.views.subdepartments',
+ 'label': "Subdepartments",
+ },
+
+ 'tailbone.views.vendors': {
+ # 'spec': 'tailbone.views.vendors',
+ 'label': "Vendors",
+ },
+
+ 'tailbone.views.products': {
+ # 'spec': 'tailbone.views.products',
+ 'label': "Products",
+ },
+
+ 'tailbone.views.brands': {
+ # 'spec': 'tailbone.views.brands',
+ 'label': "Brands",
+ },
+
+ 'tailbone.views.categories': {
+ # 'spec': 'tailbone.views.categories',
+ 'label': "Categories",
+ },
+
+ 'tailbone.views.depositlinks': {
+ # 'spec': 'tailbone.views.depositlinks',
+ 'label': "Deposit Links",
+ },
+
+ 'tailbone.views.families': {
+ # 'spec': 'tailbone.views.families',
+ 'label': "Families",
+ },
+
+ 'tailbone.views.reportcodes': {
+ # 'spec': 'tailbone.views.reportcodes',
+ 'label': "Report Codes",
+ },
+ },
+
+ 'other': {
+
+ 'tailbone.views.stores': {
+ # 'spec': 'tailbone.views.stores',
+ 'label': "Stores",
+ },
+
+ 'tailbone.views.taxes': {
+ # 'spec': 'tailbone.views.taxes',
+ 'label': "Taxes",
+ },
+ },
+ }
+
+ def configure_get_simple_settings(self):
+ settings = []
+
+ view_settings = self.collect_available_view_settings()
+ for view_section, section_settings in six.iteritems(view_settings):
+ for key in section_settings:
+ settings.append({'section': 'tailbone.includes',
+ 'option': key})
+
+ return settings
+
+ def configure_get_context(self, simple_settings=None,
+ input_file_templates=True):
+
+ # first get normal context
+ context = super(PoserViewView, self).configure_get_context(
+ simple_settings=simple_settings,
+ input_file_templates=input_file_templates)
+
+ # add available settings as sorted (key, label) options
+ view_settings = self.collect_available_view_settings()
+ for key in list(view_settings):
+ settings = view_settings[key]
+ settings = [(key, setting['label'])
+ for key, setting in six.iteritems(settings)]
+ settings.sort(key=lambda itm: itm[1])
+ view_settings[key] = settings
+ context['view_settings'] = view_settings
+
+ return context
+
+ def configure_flash_settings_saved(self):
+ super(PoserViewView, self).configure_flash_settings_saved()
+ self.request.session.flash("Please restart the web app!", 'warning')
+
+
+class PoserViewSchema(colander.MappingSchema):
+
+ key = colander.SchemaNode(colander.String())
+
+ class_name = colander.SchemaNode(colander.String())
+
+ description = colander.SchemaNode(colander.String())
+
+ # include_comments = colander.SchemaNode(colander.Bool())
+
+
+def defaults(config, **kwargs):
+ base = globals()
+
+ PoserViewView = kwargs.get('PoserViewView', base['PoserViewView'])
+ PoserViewView.defaults(config)
+
+
+def includeme(config):
+ defaults(config)