Compare commits
3 commits
a8514da107
...
f5ac66f264
Author | SHA1 | Date | |
---|---|---|---|
f5ac66f264 | |||
cd706821b2 | |||
c1afc3b3e3 |
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -5,6 +5,16 @@ All notable changes to wuttaweb will be documented in this file.
|
|||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## v0.8.0 (2024-08-15)
|
||||
|
||||
### Feat
|
||||
|
||||
- add form/grid label auto-overrides for master view
|
||||
|
||||
### Fix
|
||||
|
||||
- add `person` to template context for `PersonView.view_profile()`
|
||||
|
||||
## v0.7.0 (2024-08-15)
|
||||
|
||||
### Feat
|
||||
|
|
|
@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
|||
|
||||
[project]
|
||||
name = "WuttaWeb"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
description = "Web App for Wutta Framework"
|
||||
readme = "README.md"
|
||||
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
|
||||
|
@ -39,7 +39,7 @@ dependencies = [
|
|||
"pyramid_tm",
|
||||
"waitress",
|
||||
"WebHelpers2",
|
||||
"WuttJamaican[db]>=0.11.1",
|
||||
"WuttJamaican[db]>=0.12.0",
|
||||
"zope.sqlalchemy>=1.5",
|
||||
]
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -87,9 +87,10 @@ class PersonView(MasterView):
|
|||
|
||||
def view_profile(self, session=None):
|
||||
""" """
|
||||
instance = self.get_instance(session=session)
|
||||
person = self.get_instance(session=session)
|
||||
context = {
|
||||
'instance': instance,
|
||||
'person': person,
|
||||
'instance': person,
|
||||
}
|
||||
return self.render_to_response('view_profile', context)
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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, {})
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue