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/)
|
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).
|
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)
|
## v0.7.0 (2024-08-15)
|
||||||
|
|
||||||
### Feat
|
### Feat
|
||||||
|
|
|
@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "WuttaWeb"
|
name = "WuttaWeb"
|
||||||
version = "0.7.0"
|
version = "0.8.0"
|
||||||
description = "Web App for Wutta Framework"
|
description = "Web App for Wutta Framework"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
|
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
|
||||||
|
@ -39,7 +39,7 @@ dependencies = [
|
||||||
"pyramid_tm",
|
"pyramid_tm",
|
||||||
"waitress",
|
"waitress",
|
||||||
"WebHelpers2",
|
"WebHelpers2",
|
||||||
"WuttJamaican[db]>=0.11.1",
|
"WuttJamaican[db]>=0.12.0",
|
||||||
"zope.sqlalchemy>=1.5",
|
"zope.sqlalchemy>=1.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -87,9 +87,10 @@ class PersonView(MasterView):
|
||||||
|
|
||||||
def view_profile(self, session=None):
|
def view_profile(self, session=None):
|
||||||
""" """
|
""" """
|
||||||
instance = self.get_instance(session=session)
|
person = self.get_instance(session=session)
|
||||||
context = {
|
context = {
|
||||||
'instance': instance,
|
'person': person,
|
||||||
|
'instance': person,
|
||||||
}
|
}
|
||||||
return self.render_to_response('view_profile', context)
|
return self.render_to_response('view_profile', context)
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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, {})
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue