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:
Lance Edgar 2022-12-08 18:19:32 -06:00
parent 1a51f3d854
commit d5d9c644a2
9 changed files with 153 additions and 14 deletions

View file

@ -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']

View file

@ -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

View file

@ -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

View file

@ -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):

View file

@ -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')

View file

@ -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)

View file

@ -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)

View file

@ -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):

View file

@ -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'),