Compare commits
No commits in common. "f5ac66f264052c01d09f763b0e51a39deca98c1f" and "a8514da107bf1daf6eb93b010ea0643499d3e1c9" have entirely different histories.
f5ac66f264
...
a8514da107
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -5,16 +5,6 @@ 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.8.0"
|
version = "0.7.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.12.0",
|
"WuttJamaican[db]>=0.11.1",
|
||||||
"zope.sqlalchemy>=1.5",
|
"zope.sqlalchemy>=1.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -435,14 +435,16 @@ 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,16 +59,13 @@ 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 just convert the object to a string and return that.
|
will raise ``NotImplementeError``.
|
||||||
"""
|
"""
|
||||||
if hasattr(self.typ, 'dictify'):
|
if hasattr(self.typ, 'dictify'):
|
||||||
return self.typ.dictify(obj)
|
return self.typ.dictify(obj)
|
||||||
|
|
||||||
# TODO: this is better than raising an error, as it previously
|
class_name = self.typ.__class__.__name__
|
||||||
# did, but seems like troubleshooting problems may often lead
|
raise NotImplementedError(f"you must define {class_name}.dictify()")
|
||||||
# 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,12 +82,6 @@ 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.
|
||||||
|
@ -119,7 +113,6 @@ class Grid:
|
||||||
key=None,
|
key=None,
|
||||||
columns=None,
|
columns=None,
|
||||||
data=None,
|
data=None,
|
||||||
labels={},
|
|
||||||
renderers={},
|
renderers={},
|
||||||
actions=[],
|
actions=[],
|
||||||
linked_columns=[],
|
linked_columns=[],
|
||||||
|
@ -129,7 +122,6 @@ 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 []
|
||||||
|
@ -228,32 +220,6 @@ 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.
|
||||||
|
@ -410,7 +376,7 @@ class Grid:
|
||||||
for name in self.columns:
|
for name in self.columns:
|
||||||
columns.append({
|
columns.append({
|
||||||
'field': name,
|
'field': name,
|
||||||
'label': self.get_label(name),
|
'label': self.app.make_title(name),
|
||||||
})
|
})
|
||||||
return columns
|
return columns
|
||||||
|
|
||||||
|
@ -464,7 +430,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.get(key, None)
|
value = record[key]
|
||||||
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,7 +33,6 @@ 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):
|
||||||
|
@ -804,16 +803,6 @@ 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.
|
||||||
|
@ -960,60 +949,6 @@ 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`
|
||||||
|
@ -1137,8 +1072,6 @@ 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)
|
||||||
|
|
||||||
|
@ -1378,8 +1311,6 @@ 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,10 +87,9 @@ class PersonView(MasterView):
|
||||||
|
|
||||||
def view_profile(self, session=None):
|
def view_profile(self, session=None):
|
||||||
""" """
|
""" """
|
||||||
person = self.get_instance(session=session)
|
instance = self.get_instance(session=session)
|
||||||
context = {
|
context = {
|
||||||
'person': person,
|
'instance': instance,
|
||||||
'instance': person,
|
|
||||||
}
|
}
|
||||||
return self.render_to_response('view_profile', context)
|
return self.render_to_response('view_profile', context)
|
||||||
|
|
||||||
|
|
|
@ -22,10 +22,9 @@ 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 is converted to string
|
# unsupported type raises error
|
||||||
node = mod.ObjectNode(colander.String())
|
node = mod.ObjectNode(colander.String())
|
||||||
value = node.dictify(person)
|
self.assertRaises(NotImplementedError, 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,30 +81,6 @@ 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,7 +10,6 @@ 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
|
||||||
|
|
||||||
|
@ -332,14 +331,6 @@ 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()
|
||||||
|
@ -437,36 +428,6 @@ 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