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`. Node overrides are tracked via :attr:`nodes`.
""" """
from wuttaweb.forms.schema import ObjectNode
if isinstance(nodeinfo, colander.SchemaNode): if isinstance(nodeinfo, colander.SchemaNode):
# assume nodeinfo is a complete node # assume nodeinfo is a complete node
node = nodeinfo node = nodeinfo
else: # assume nodeinfo is a schema type else: # assume nodeinfo is a schema type
kwargs.setdefault('name', key) kwargs.setdefault('name', key)
from wuttaweb.forms.schema import ObjectNode
# node = colander.SchemaNode(nodeinfo, **kwargs)
node = ObjectNode(nodeinfo, **kwargs) node = ObjectNode(nodeinfo, **kwargs)
self.nodes[key] = node self.nodes[key] = node

View file

@ -59,13 +59,16 @@ class ObjectNode(colander.SchemaNode):
:class:`ObjectRef`. :class:`ObjectRef`.
If the node's type does not have a ``dictify()`` method, this 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'): if hasattr(self.typ, 'dictify'):
return self.typ.dictify(obj) return self.typ.dictify(obj)
class_name = self.typ.__class__.__name__ # TODO: this is better than raising an error, as it previously
raise NotImplementedError(f"you must define {class_name}.dictify()") # 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): def objectify(self, value):
""" """

View file

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

View file

@ -33,6 +33,7 @@ from webhelpers2.html import HTML
from wuttaweb.views import View from wuttaweb.views import View
from wuttaweb.util import get_form_data, get_model_fields from wuttaweb.util import get_form_data, get_model_fields
from wuttaweb.db import Session from wuttaweb.db import Session
from wuttjamaican.util import get_class_hierarchy
class MasterView(View): class MasterView(View):
@ -803,6 +804,16 @@ class MasterView(View):
# support methods # 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): def has_perm(self, name):
""" """
Shortcut to check if current user has the given permission. Shortcut to check if current user has the given permission.
@ -949,6 +960,60 @@ class MasterView(View):
route_prefix = self.get_route_prefix() route_prefix = self.get_route_prefix()
return self.request.route_url(route_prefix, **kwargs) 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): def make_model_grid(self, session=None, **kwargs):
""" """
Create and return a :class:`~wuttaweb.grids.base.Grid` Create and return a :class:`~wuttaweb.grids.base.Grid`
@ -1072,6 +1137,8 @@ class MasterView(View):
if 'uuid' in grid.columns: if 'uuid' in grid.columns:
grid.columns.remove('uuid') grid.columns.remove('uuid')
self.set_labels(grid)
for key in self.get_model_key(): for key in self.get_model_key():
grid.set_link(key) grid.set_link(key)
@ -1311,6 +1378,8 @@ class MasterView(View):
""" """
form.remove('uuid') form.remove('uuid')
self.set_labels(form)
if self.editing: if self.editing:
for key in self.get_model_key(): for key in self.get_model_key():
form.set_readonly(key) form.set_readonly(key)

View file

@ -22,9 +22,10 @@ class TestObjectNode(DataTestCase):
model = self.app.model model = self.app.model
person = model.Person(full_name="Betty Boop") person = model.Person(full_name="Betty Boop")
# unsupported type raises error # unsupported type is converted to string
node = mod.ObjectNode(colander.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 # but supported type can dictify
node = mod.ObjectNode(mod.PersonRef(self.request)) node = mod.ObjectNode(mod.PersonRef(self.request))

View file

@ -81,6 +81,30 @@ class TestGrid(TestCase):
grid.remove('two', 'three') grid.remove('two', 'three')
self.assertEqual(grid.columns, ['one', 'four']) 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): def test_set_renderer(self):
grid = self.make_grid(columns=['foo', 'bar']) grid = self.make_grid(columns=['foo', 'bar'])
self.assertEqual(grid.renderers, {}) self.assertEqual(grid.renderers, {})

View file

@ -10,6 +10,7 @@ from pyramid.httpexceptions import HTTPNotFound
from wuttjamaican.conf import WuttaConfig from wuttjamaican.conf import WuttaConfig
from wuttaweb.views import master from wuttaweb.views import master
from wuttaweb.views import View
from wuttaweb.subscribers import new_request_set_user from wuttaweb.subscribers import new_request_set_user
from tests.util import WebTestCase from tests.util import WebTestCase
@ -331,6 +332,14 @@ class TestMasterView(WebTestCase):
# support methods # 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): def test_has_perm(self):
model = self.app.model model = self.app.model
auth = self.app.get_auth_handler() auth = self.app.get_auth_handler()
@ -428,6 +437,36 @@ class TestMasterView(WebTestCase):
self.assertEqual(view.get_index_title(), "Wutta Widgets") self.assertEqual(view.get_index_title(), "Wutta Widgets")
del master.MasterView.model_title_plural 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): def test_make_model_grid(self):
model = self.app.model model = self.app.model