Add initial/basic support for configuring "included views"
also stub for managing "poser views"
This commit is contained in:
parent
33abeb1aca
commit
66a15fb9a1
|
@ -27,21 +27,42 @@ App Menus
|
||||||
from __future__ import unicode_literals, absolute_import
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
import re
|
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
|
from tailbone.db import Session
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def make_simple_menus(request):
|
def make_simple_menus(request):
|
||||||
"""
|
"""
|
||||||
Build the main menu list for the app.
|
Build the main menu list for the app.
|
||||||
"""
|
"""
|
||||||
# first try to make menus from config
|
# first try to make menus from config, but this is highly
|
||||||
raw_menus = make_menus_from_config(request)
|
# 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:
|
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(
|
menus_module = import_module_path(
|
||||||
request.rattail_config.require('tailbone', 'menus'))
|
request.rattail_config.require('tailbone', 'menus'))
|
||||||
if not hasattr(menus_module, 'simple_menus') or not callable(menus_module.simple_menus):
|
if not hasattr(menus_module, 'simple_menus') or not callable(menus_module.simple_menus):
|
||||||
|
|
41
tailbone/templates/poser/views/configure.mako
Normal file
41
tailbone/templates/poser/views/configure.mako
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
## -*- coding: utf-8; -*-
|
||||||
|
<%inherit file="/configure.mako" />
|
||||||
|
|
||||||
|
<%def name="form_content()">
|
||||||
|
|
||||||
|
<p class="block has-text-weight-bold is-italic">
|
||||||
|
NB. Any changes made here will require an app restart!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 class="block is-size-3">Tailbone Views</h3>
|
||||||
|
|
||||||
|
<h4 class="block is-size-4">People</h4>
|
||||||
|
% for key, label in view_settings['people']:
|
||||||
|
${self.simple_flag(key, label)}
|
||||||
|
% endfor
|
||||||
|
|
||||||
|
<h4 class="block is-size-4">Products</h4>
|
||||||
|
% for key, label in view_settings['products']:
|
||||||
|
${self.simple_flag(key, label)}
|
||||||
|
% endfor
|
||||||
|
|
||||||
|
<h4 class="block is-size-4">Other</h4>
|
||||||
|
% for key, label in view_settings['other']:
|
||||||
|
${self.simple_flag(key, label)}
|
||||||
|
% endfor
|
||||||
|
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="simple_flag(key, label)">
|
||||||
|
<b-field label="${label}" horizontal>
|
||||||
|
<b-select name="tailbone.includes.${key}"
|
||||||
|
v-model="simpleSettings['tailbone.includes.${key}']"
|
||||||
|
@input="settingsNeedSaved = true">
|
||||||
|
<option :value="null">(disabled)</option>
|
||||||
|
<option value="${key}">${key}</option>
|
||||||
|
</b-select>
|
||||||
|
</b-field>
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
|
${parent.body()}
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2021 Lance Edgar
|
# Copyright © 2010-2022 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -251,3 +251,24 @@ def route_exists(request, route_name):
|
||||||
mapper = reg.getUtility(IRoutesMapper)
|
mapper = reg.getUtility(IRoutesMapper)
|
||||||
route = mapper.get_route(route_name)
|
route = mapper.get_route(route_name)
|
||||||
return bool(route)
|
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()
|
||||||
|
|
|
@ -4245,12 +4245,15 @@ class MasterView(View):
|
||||||
settings = self.configure_gather_settings(data)
|
settings = self.configure_gather_settings(data)
|
||||||
self.configure_remove_settings()
|
self.configure_remove_settings()
|
||||||
self.configure_save_settings(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())
|
return self.redirect(self.request.current_route_url())
|
||||||
|
|
||||||
context = self.configure_get_context()
|
context = self.configure_get_context()
|
||||||
return self.render_to_response('configure', 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):
|
def configure_process_uploads(self, uploads, data):
|
||||||
if self.has_input_file_templates:
|
if self.has_input_file_templates:
|
||||||
templatesdir = os.path.join(self.rattail_config.datadir(),
|
templatesdir = os.path.join(self.rattail_config.datadir(),
|
||||||
|
|
|
@ -29,3 +29,4 @@ from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
config.include('tailbone.views.poser.reports')
|
config.include('tailbone.views.poser.reports')
|
||||||
|
config.include('tailbone.views.poser.views')
|
||||||
|
|
241
tailbone/views/poser/views.py
Normal file
241
tailbone/views/poser/views.py
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
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)
|
Loading…
Reference in a new issue