2
0
Fork 0

feat: add form/grid label auto-overrides for master view

This commit is contained in:
Lance Edgar 2024-08-15 20:51:36 -05:00
parent c1afc3b3e3
commit cd706821b2
7 changed files with 179 additions and 11 deletions

View file

@ -435,16 +435,14 @@ class Form:
Node overrides are tracked via :attr:`nodes`.
"""
from wuttaweb.forms.schema import ObjectNode
if isinstance(nodeinfo, colander.SchemaNode):
# assume nodeinfo is a complete node
node = nodeinfo
else: # assume nodeinfo is a schema type
kwargs.setdefault('name', key)
from wuttaweb.forms.schema import ObjectNode
# node = colander.SchemaNode(nodeinfo, **kwargs)
node = ObjectNode(nodeinfo, **kwargs)
self.nodes[key] = node

View file

@ -59,13 +59,16 @@ class ObjectNode(colander.SchemaNode):
:class:`ObjectRef`.
If the node's type does not have a ``dictify()`` method, this
will raise ``NotImplementeError``.
will just convert the object to a string and return that.
"""
if hasattr(self.typ, 'dictify'):
return self.typ.dictify(obj)
class_name = self.typ.__class__.__name__
raise NotImplementedError(f"you must define {class_name}.dictify()")
# TODO: this is better than raising an error, as it previously
# did, but seems like troubleshooting problems may often lead
# one here.. i suspect this needs to do something smarter but
# not sure what that is yet
return str(obj)
def objectify(self, value):
"""

View file

@ -82,6 +82,12 @@ class Grid:
model records) or else an object capable of producing such a
list, e.g. SQLAlchemy query.
.. attribute:: labels
Dict of column label overrides.
See also :meth:`get_label()` and :meth:`set_label()`.
.. attribute:: renderers
Dict of column (cell) value renderer overrides.
@ -113,6 +119,7 @@ class Grid:
key=None,
columns=None,
data=None,
labels={},
renderers={},
actions=[],
linked_columns=[],
@ -122,6 +129,7 @@ class Grid:
self.model_class = model_class
self.key = key
self.data = data
self.labels = labels or {}
self.renderers = renderers or {}
self.actions = actions or []
self.linked_columns = linked_columns or []
@ -220,6 +228,32 @@ class Grid:
if key in self.columns:
self.columns.remove(key)
def set_label(self, key, label):
"""
Set/override the label for a column.
:param key: Name of column.
:param label: New label for the column header.
See also :meth:`get_label()`.
Label overrides are tracked via :attr:`labels`.
"""
self.labels[key] = label
def get_label(self, key):
"""
Returns the label text for a given column.
If no override is defined, the label is derived from ``key``.
See also :meth:`set_label()`.
"""
if key in self.labels:
return self.labels[key]
return self.app.make_title(key)
def set_renderer(self, key, renderer, **kwargs):
"""
Set/override the value renderer for a column.
@ -376,7 +410,7 @@ class Grid:
for name in self.columns:
columns.append({
'field': name,
'label': self.app.make_title(name),
'label': self.get_label(name),
})
return columns
@ -430,7 +464,7 @@ class Grid:
# customize value rendering where applicable
for key in self.renderers:
value = record[key]
value = record.get(key, None)
record[key] = self.renderers[key](original_record, key, value)
# add action urls to each record

View file

@ -33,6 +33,7 @@ from webhelpers2.html import HTML
from wuttaweb.views import View
from wuttaweb.util import get_form_data, get_model_fields
from wuttaweb.db import Session
from wuttjamaican.util import get_class_hierarchy
class MasterView(View):
@ -803,6 +804,16 @@ class MasterView(View):
# support methods
##############################
def get_class_hierarchy(self, topfirst=True):
"""
Convenience to return a list of classes from which the current
class inherits.
This is a wrapper around
:func:`wuttjamaican.util.get_class_hierarchy()`.
"""
return get_class_hierarchy(self.__class__, topfirst=topfirst)
def has_perm(self, name):
"""
Shortcut to check if current user has the given permission.
@ -949,6 +960,60 @@ class MasterView(View):
route_prefix = self.get_route_prefix()
return self.request.route_url(route_prefix, **kwargs)
def set_labels(self, obj):
"""
Set label overrides on a form or grid, based on what is
defined by the view class and its parent class(es).
This is called automatically from :meth:`configure_grid()` and
:meth:`configure_form()`.
This calls :meth:`collect_labels()` to find everything, then
it assigns the labels using one of (based on ``obj`` type):
* :func:`wuttaweb.forms.base.Form.set_label()`
* :func:`wuttaweb.grids.base.Grid.set_label()`
:param obj: Either a :class:`~wuttaweb.grids.base.Grid` or a
:class:`~wuttaweb.forms.base.Form` instance.
"""
labels = self.collect_labels()
for key, label in labels.items():
obj.set_label(key, label)
def collect_labels(self):
"""
Collect all labels defined by the view class and/or its parents.
A master view can declare labels via class-level attribute,
like so::
from wuttaweb.views import MasterView
class WidgetView(MasterView):
labels = {
'id': "Widget ID",
'serial_no': "Serial Number",
}
All such labels, defined by any class from which the master
view inherits, will be returned. However if the same label
key is defined by multiple classes, the "subclass" always
wins.
Labels defined in this way will apply to both forms and grids.
See also :meth:`set_labels()`.
:returns: Dict of all labels found.
"""
labels = {}
hierarchy = self.get_class_hierarchy()
for cls in hierarchy:
if hasattr(cls, 'labels'):
labels.update(cls.labels)
return labels
def make_model_grid(self, session=None, **kwargs):
"""
Create and return a :class:`~wuttaweb.grids.base.Grid`
@ -1072,6 +1137,8 @@ class MasterView(View):
if 'uuid' in grid.columns:
grid.columns.remove('uuid')
self.set_labels(grid)
for key in self.get_model_key():
grid.set_link(key)
@ -1311,6 +1378,8 @@ class MasterView(View):
"""
form.remove('uuid')
self.set_labels(form)
if self.editing:
for key in self.get_model_key():
form.set_readonly(key)

View file

@ -22,9 +22,10 @@ class TestObjectNode(DataTestCase):
model = self.app.model
person = model.Person(full_name="Betty Boop")
# unsupported type raises error
# unsupported type is converted to string
node = mod.ObjectNode(colander.String())
self.assertRaises(NotImplementedError, node.dictify, person)
value = node.dictify(person)
self.assertEqual(value, "Betty Boop")
# but supported type can dictify
node = mod.ObjectNode(mod.PersonRef(self.request))

View file

@ -81,6 +81,30 @@ class TestGrid(TestCase):
grid.remove('two', 'three')
self.assertEqual(grid.columns, ['one', 'four'])
def test_set_label(self):
grid = self.make_grid(columns=['foo', 'bar'])
self.assertEqual(grid.labels, {})
# basic
grid.set_label('foo', "Foo Fighters")
self.assertEqual(grid.labels['foo'], "Foo Fighters")
# can replace label
grid.set_label('foo', "Different")
self.assertEqual(grid.labels['foo'], "Different")
self.assertEqual(grid.get_label('foo'), "Different")
def test_get_label(self):
grid = self.make_grid(columns=['foo', 'bar'])
self.assertEqual(grid.labels, {})
# default derived from key
self.assertEqual(grid.get_label('foo'), "Foo")
# can override
grid.set_label('foo', "Different")
self.assertEqual(grid.get_label('foo'), "Different")
def test_set_renderer(self):
grid = self.make_grid(columns=['foo', 'bar'])
self.assertEqual(grid.renderers, {})

View file

@ -10,6 +10,7 @@ from pyramid.httpexceptions import HTTPNotFound
from wuttjamaican.conf import WuttaConfig
from wuttaweb.views import master
from wuttaweb.views import View
from wuttaweb.subscribers import new_request_set_user
from tests.util import WebTestCase
@ -331,6 +332,14 @@ class TestMasterView(WebTestCase):
# support methods
##############################
def test_get_class_hierarchy(self):
class MyView(master.MasterView):
pass
view = MyView(self.request)
classes = view.get_class_hierarchy()
self.assertEqual(classes, [View, master.MasterView, MyView])
def test_has_perm(self):
model = self.app.model
auth = self.app.get_auth_handler()
@ -428,6 +437,36 @@ class TestMasterView(WebTestCase):
self.assertEqual(view.get_index_title(), "Wutta Widgets")
del master.MasterView.model_title_plural
def test_collect_labels(self):
# no labels by default
view = self.make_view()
labels = view.collect_labels()
self.assertEqual(labels, {})
# labels come from all classes; subclass wins
with patch.object(View, 'labels', new={'foo': "Foo", 'bar': "Bar"}, create=True):
with patch.object(master.MasterView, 'labels', new={'foo': "FOO FIGHTERS"}, create=True):
view = self.make_view()
labels = view.collect_labels()
self.assertEqual(labels, {'foo': "FOO FIGHTERS", 'bar': "Bar"})
def test_set_labels(self):
model = self.app.model
with patch.object(master.MasterView, 'model_class', new=model.Setting, create=True):
# no labels by default
view = self.make_view()
grid = view.make_model_grid(session=self.session)
view.set_labels(grid)
self.assertEqual(grid.labels, {})
# labels come from all classes; subclass wins
with patch.object(master.MasterView, 'labels', new={'name': "SETTING NAME"}, create=True):
view = self.make_view()
view.set_labels(grid)
self.assertEqual(grid.labels, {'name': "SETTING NAME"})
def test_make_model_grid(self):
model = self.app.model