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 name="simple_flag(key, label)"> + + + + + + + + + +${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)