Add the ViewSupplement concept
also fix cell-class for grid columns. cannot use "raw" fieldname because in some cases (e.g. 'number', 'rate') Bulma may interpret that as actually meaning something, and affect the display
This commit is contained in:
parent
1a51f3d854
commit
d5d9c644a2
|
@ -177,6 +177,7 @@ def make_pyramid_config(settings, configure_csrf=True):
|
||||||
# and some similar magic for certain master views
|
# and some similar magic for certain master views
|
||||||
config.add_directive('add_tailbone_index_page', 'tailbone.app.add_index_page')
|
config.add_directive('add_tailbone_index_page', 'tailbone.app.add_index_page')
|
||||||
config.add_directive('add_tailbone_config_page', 'tailbone.app.add_config_page')
|
config.add_directive('add_tailbone_config_page', 'tailbone.app.add_config_page')
|
||||||
|
config.add_directive('add_tailbone_view_supplement', 'tailbone.app.add_view_supplement')
|
||||||
|
|
||||||
config.add_directive('add_tailbone_websocket', 'tailbone.app.add_websocket')
|
config.add_directive('add_tailbone_websocket', 'tailbone.app.add_websocket')
|
||||||
|
|
||||||
|
@ -239,6 +240,17 @@ def add_config_page(config, route_name, label, permission):
|
||||||
config.action(None, action)
|
config.action(None, action)
|
||||||
|
|
||||||
|
|
||||||
|
def add_view_supplement(config, route_prefix, cls):
|
||||||
|
"""
|
||||||
|
Register a master view supplement for the app.
|
||||||
|
"""
|
||||||
|
def action():
|
||||||
|
supplements = config.get_settings().get('tailbone_view_supplements', {})
|
||||||
|
supplements.setdefault(route_prefix, []).append(cls)
|
||||||
|
config.add_settings({'tailbone_view_supplements': supplements})
|
||||||
|
config.action(None, action)
|
||||||
|
|
||||||
|
|
||||||
def establish_theme(settings):
|
def establish_theme(settings):
|
||||||
rattail_config = settings['rattail_config']
|
rattail_config = settings['rattail_config']
|
||||||
|
|
||||||
|
|
|
@ -221,7 +221,7 @@
|
||||||
% if grid.is_searchable(column['field']):
|
% if grid.is_searchable(column['field']):
|
||||||
searchable
|
searchable
|
||||||
% endif
|
% endif
|
||||||
cell-class="${column['field']}"
|
cell-class="c_${column['field']}"
|
||||||
% if grid.has_click_handler(column['field']):
|
% if grid.has_click_handler(column['field']):
|
||||||
@click.native="${grid.click_handlers[column['field']]}"
|
@click.native="${grid.click_handlers[column['field']]}"
|
||||||
% endif
|
% endif
|
||||||
|
|
|
@ -267,20 +267,20 @@
|
||||||
% if use_buefy:
|
% if use_buefy:
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
% if allow_edit_catalog_unit_cost:
|
% if allow_edit_catalog_unit_cost:
|
||||||
td.catalog_unit_cost {
|
td.c_catalog_unit_cost {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: #fcc;
|
background-color: #fcc;
|
||||||
}
|
}
|
||||||
tr.catalog_cost_confirmed td.catalog_unit_cost {
|
tr.catalog_cost_confirmed td.c_catalog_unit_cost {
|
||||||
background-color: #cfc;
|
background-color: #cfc;
|
||||||
}
|
}
|
||||||
% endif
|
% endif
|
||||||
% if allow_edit_invoice_unit_cost:
|
% if allow_edit_invoice_unit_cost:
|
||||||
td.invoice_unit_cost {
|
td.c_invoice_unit_cost {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: #fcc;
|
background-color: #fcc;
|
||||||
}
|
}
|
||||||
tr.invoice_cost_confirmed td.invoice_unit_cost {
|
tr.invoice_cost_confirmed td.c_invoice_unit_cost {
|
||||||
background-color: #cfc;
|
background-color: #cfc;
|
||||||
}
|
}
|
||||||
% endif
|
% endif
|
||||||
|
|
|
@ -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.
|
||||||
#
|
#
|
||||||
|
@ -27,7 +27,7 @@ Pyramid Views
|
||||||
from __future__ import unicode_literals, absolute_import
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
from .core import View
|
from .core import View
|
||||||
from .master import MasterView
|
from .master import MasterView, ViewSupplement
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
|
|
|
@ -115,6 +115,10 @@ class CustomerView(MasterView):
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
super(CustomerView, self).configure_grid(g)
|
super(CustomerView, self).configure_grid(g)
|
||||||
|
model = self.model
|
||||||
|
|
||||||
|
# number
|
||||||
|
g.set_link('number')
|
||||||
|
|
||||||
# name
|
# name
|
||||||
g.filters['name'].default_active = True
|
g.filters['name'].default_active = True
|
||||||
|
@ -158,7 +162,6 @@ class CustomerView(MasterView):
|
||||||
g.filters['active_in_pos'].default_verb = 'is_true'
|
g.filters['active_in_pos'].default_verb = 'is_true'
|
||||||
|
|
||||||
g.set_link('id')
|
g.set_link('id')
|
||||||
g.set_link('number')
|
|
||||||
g.set_link('name')
|
g.set_link('name')
|
||||||
g.set_link('person')
|
g.set_link('person')
|
||||||
g.set_link('email')
|
g.set_link('email')
|
||||||
|
|
|
@ -66,6 +66,7 @@ class DepartmentView(MasterView):
|
||||||
|
|
||||||
has_rows = True
|
has_rows = True
|
||||||
model_row_class = model.Product
|
model_row_class = model.Product
|
||||||
|
rows_title = "Products"
|
||||||
|
|
||||||
row_labels = {
|
row_labels = {
|
||||||
'upc': "UPC",
|
'upc': "UPC",
|
||||||
|
@ -83,13 +84,18 @@ class DepartmentView(MasterView):
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
super(DepartmentView, self).configure_grid(g)
|
super(DepartmentView, self).configure_grid(g)
|
||||||
|
|
||||||
|
# number
|
||||||
|
g.set_sort_defaults('number')
|
||||||
|
g.set_link('number')
|
||||||
|
|
||||||
|
# name
|
||||||
g.filters['name'].default_active = True
|
g.filters['name'].default_active = True
|
||||||
g.filters['name'].default_verb = 'contains'
|
g.filters['name'].default_verb = 'contains'
|
||||||
g.set_sort_defaults('number')
|
g.set_link('name')
|
||||||
|
|
||||||
g.set_type('product', 'boolean')
|
g.set_type('product', 'boolean')
|
||||||
g.set_type('personnel', 'boolean')
|
g.set_type('personnel', 'boolean')
|
||||||
g.set_link('number')
|
|
||||||
g.set_link('name')
|
|
||||||
|
|
||||||
def configure_form(self, f):
|
def configure_form(self, f):
|
||||||
super(DepartmentView, self).configure_form(f)
|
super(DepartmentView, self).configure_form(f)
|
||||||
|
|
|
@ -259,6 +259,8 @@ class MasterView(View):
|
||||||
Collect all labels defined within the master class hierarchy.
|
Collect all labels defined within the master class hierarchy.
|
||||||
"""
|
"""
|
||||||
labels = {}
|
labels = {}
|
||||||
|
for supp in self.iter_view_supplements():
|
||||||
|
labels.update(supp.labels)
|
||||||
hierarchy = self.get_class_hierarchy()
|
hierarchy = self.get_class_hierarchy()
|
||||||
for cls in hierarchy:
|
for cls in hierarchy:
|
||||||
if hasattr(cls, 'labels'):
|
if hasattr(cls, 'labels'):
|
||||||
|
@ -473,6 +475,9 @@ class MasterView(View):
|
||||||
|
|
||||||
self.configure_column_product_key(grid)
|
self.configure_column_product_key(grid)
|
||||||
|
|
||||||
|
for supp in self.iter_view_supplements():
|
||||||
|
supp.configure_grid(grid)
|
||||||
|
|
||||||
def grid_extra_class(self, obj, i):
|
def grid_extra_class(self, obj, i):
|
||||||
"""
|
"""
|
||||||
Returns string of extra class(es) for the table row corresponding to
|
Returns string of extra class(es) for the table row corresponding to
|
||||||
|
@ -1226,7 +1231,10 @@ class MasterView(View):
|
||||||
If applicable, should return a list of child classes which should be
|
If applicable, should return a list of child classes which should be
|
||||||
considered when querying for version history of an object.
|
considered when querying for version history of an object.
|
||||||
"""
|
"""
|
||||||
return []
|
classes = []
|
||||||
|
for supp in self.iter_view_supplements():
|
||||||
|
classes.extend(supp.get_version_child_classes())
|
||||||
|
return classes
|
||||||
|
|
||||||
def normalize_version_child_classes(self):
|
def normalize_version_child_classes(self):
|
||||||
classes = []
|
classes = []
|
||||||
|
@ -2707,6 +2715,9 @@ class MasterView(View):
|
||||||
if not self.has_perm('view_global'):
|
if not self.has_perm('view_global'):
|
||||||
query = query.filter(model_class.local_only == True)
|
query = query.filter(model_class.local_only == True)
|
||||||
|
|
||||||
|
for supp in self.iter_view_supplements():
|
||||||
|
query = supp.get_grid_query(query)
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
def get_effective_query(self, session=None, **kwargs):
|
def get_effective_query(self, session=None, **kwargs):
|
||||||
|
@ -3826,6 +3837,17 @@ class MasterView(View):
|
||||||
defaults.update(kwargs)
|
defaults.update(kwargs)
|
||||||
return defaults
|
return defaults
|
||||||
|
|
||||||
|
def iter_view_supplements(self):
|
||||||
|
"""
|
||||||
|
Iterate over all registered supplements for this master view.
|
||||||
|
"""
|
||||||
|
supplements = self.request.registry.settings['tailbone_view_supplements']
|
||||||
|
route_prefix = self.get_route_prefix()
|
||||||
|
if supplements and route_prefix in supplements:
|
||||||
|
for cls in supplements[route_prefix]:
|
||||||
|
supp = cls(self)
|
||||||
|
yield supp
|
||||||
|
|
||||||
def configure_form(self, form):
|
def configure_form(self, form):
|
||||||
"""
|
"""
|
||||||
Configure the main "desktop" form for the view's data model.
|
Configure the main "desktop" form for the view's data model.
|
||||||
|
@ -3834,6 +3856,9 @@ class MasterView(View):
|
||||||
|
|
||||||
self.configure_field_product_key(form)
|
self.configure_field_product_key(form)
|
||||||
|
|
||||||
|
for supp in self.iter_view_supplements():
|
||||||
|
supp.configure_form(form)
|
||||||
|
|
||||||
def validate_form(self, form):
|
def validate_form(self, form):
|
||||||
if form.validate(newstyle=True):
|
if form.validate(newstyle=True):
|
||||||
self.form_deserialized = form.validated
|
self.form_deserialized = form.validated
|
||||||
|
@ -5009,3 +5034,94 @@ class MasterView(View):
|
||||||
'{}/rows/{{row_uuid}}/delete'.format(instance_url_prefix))
|
'{}/rows/{{row_uuid}}/delete'.format(instance_url_prefix))
|
||||||
config.add_view(cls, attr='delete_row', route_name='{}.delete_row'.format(route_prefix),
|
config.add_view(cls, attr='delete_row', route_name='{}.delete_row'.format(route_prefix),
|
||||||
permission='{}.delete_row'.format(permission_prefix))
|
permission='{}.delete_row'.format(permission_prefix))
|
||||||
|
|
||||||
|
|
||||||
|
class ViewSupplement(object):
|
||||||
|
"""
|
||||||
|
Base class for view "supplements" - which are sort of like plugins
|
||||||
|
which can "supplement" certain aspects of the view.
|
||||||
|
|
||||||
|
Instead of subclassing a master view and "supplementing" it via
|
||||||
|
method overrides etc., packages can instead define one or more
|
||||||
|
``ViewSupplement`` classes. All such supplements are registered
|
||||||
|
so they can be located; their logic is then merged into the
|
||||||
|
appropriate master view at runtime.
|
||||||
|
|
||||||
|
The primary use case for this is within integration packages, such
|
||||||
|
as tailbone-corepos and the like. A truly custom app might want
|
||||||
|
supplemental logic from multiple integration packages, in which
|
||||||
|
case the "subclassing" approach sort of falls apart.
|
||||||
|
|
||||||
|
:attribute:: labels
|
||||||
|
|
||||||
|
This can be a dict of extra field labels to be used by the
|
||||||
|
master view. Same meaning as for
|
||||||
|
:attr:`tailbone.views.master.MasterView.labels`.
|
||||||
|
"""
|
||||||
|
labels = {}
|
||||||
|
|
||||||
|
def __init__(self, master):
|
||||||
|
self.master = master
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model(self):
|
||||||
|
return self.master.model
|
||||||
|
|
||||||
|
def get_grid_query(self, query):
|
||||||
|
"""
|
||||||
|
Return the "base" query for the grid. This is invoked from
|
||||||
|
within :meth:`tailbone.views.master.MasterView.query()`.
|
||||||
|
|
||||||
|
A typical grid query is
|
||||||
|
essentially:
|
||||||
|
|
||||||
|
.. code-block:: sql
|
||||||
|
|
||||||
|
SELECT * FROM mytable
|
||||||
|
|
||||||
|
But when a schema extension is in "primary" use, meaning for
|
||||||
|
instance one of the main grid columns displays extension data,
|
||||||
|
it may be helpful for the base query to join the extension
|
||||||
|
table, as opposed to doing a "just in time" join based on
|
||||||
|
sorting and/or filters:
|
||||||
|
|
||||||
|
.. code-block:: sql
|
||||||
|
|
||||||
|
SELECT * FROM mytable m
|
||||||
|
LEFT OUTER JOIN myextension e ON e.uuid = m.uuid
|
||||||
|
|
||||||
|
This is accomplished by subjecting the current base query to a
|
||||||
|
join, e.g. something like::
|
||||||
|
|
||||||
|
model = self.model
|
||||||
|
query = query.outerjoin(model.MyExtension)
|
||||||
|
return query
|
||||||
|
"""
|
||||||
|
return query
|
||||||
|
|
||||||
|
def configure_grid(self, g):
|
||||||
|
"""
|
||||||
|
Configure the grid as needed, e.g. add columns, and set
|
||||||
|
renderers etc. for them.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def configure_form(self, f):
|
||||||
|
"""
|
||||||
|
Configure the form as needed, e.g. add fields, and set
|
||||||
|
renderers, default values etc. for them.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_version_child_classes(self):
|
||||||
|
"""
|
||||||
|
Return a list of additional "version child classes" which are
|
||||||
|
to be taken into account when displaying version history for a
|
||||||
|
given record.
|
||||||
|
|
||||||
|
See also
|
||||||
|
:meth:`tailbone.views.master.MasterView.get_version_child_classes()`.
|
||||||
|
"""
|
||||||
|
return []
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def defaults(cls, config):
|
||||||
|
config.add_tailbone_view_supplement(cls.route_prefix, cls)
|
||||||
|
|
|
@ -85,6 +85,9 @@ class SubdepartmentView(MasterView):
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
super(SubdepartmentView, self).configure_grid(g)
|
super(SubdepartmentView, self).configure_grid(g)
|
||||||
|
|
||||||
|
# number
|
||||||
|
g.set_link('number')
|
||||||
|
|
||||||
# name
|
# name
|
||||||
g.filters['name'].default_active = True
|
g.filters['name'].default_active = True
|
||||||
g.filters['name'].default_verb = 'contains'
|
g.filters['name'].default_verb = 'contains'
|
||||||
|
@ -95,7 +98,6 @@ class SubdepartmentView(MasterView):
|
||||||
g.set_sorter('department', model.Department.name)
|
g.set_sorter('department', model.Department.name)
|
||||||
g.set_filter('department', model.Department.name)
|
g.set_filter('department', model.Department.name)
|
||||||
|
|
||||||
g.set_link('number')
|
|
||||||
g.set_link('name')
|
g.set_link('name')
|
||||||
|
|
||||||
def configure_form(self, f):
|
def configure_form(self, f):
|
||||||
|
|
2
tailbone/views/vendors/core.py
vendored
2
tailbone/views/vendors/core.py
vendored
|
@ -165,7 +165,7 @@ class VendorView(MasterView):
|
||||||
self.Session.delete(cost)
|
self.Session.delete(cost)
|
||||||
|
|
||||||
def get_version_child_classes(self):
|
def get_version_child_classes(self):
|
||||||
return [
|
return super(VendorView, self).get_version_child_classes() + [
|
||||||
(model.VendorPhoneNumber, 'parent_uuid'),
|
(model.VendorPhoneNumber, 'parent_uuid'),
|
||||||
(model.VendorEmailAddress, 'parent_uuid'),
|
(model.VendorEmailAddress, 'parent_uuid'),
|
||||||
(model.VendorContact, 'vendor_uuid'),
|
(model.VendorContact, 'vendor_uuid'),
|
||||||
|
|
Loading…
Reference in a new issue