3
0
Fork 0

Compare commits

...

6 commits

Author SHA1 Message Date
064b1b6d8a bump: version 0.29.1 → 0.29.2 2026-03-10 10:58:06 -05:00
24f7d7ac0a fix: add base class for WuttaList schema type 2026-03-10 09:10:14 -05:00
ceec4714fa fix: skip custom rendering for missing grid columns 2026-03-09 22:07:29 -05:00
19cf6cf03a fix: serialize None to null, for ObjectRef schema type
not sure how this didn't come up until now..anyway hopefully it's the
right thing but we'll see
2026-03-08 12:26:21 -05:00
634c1b1fe5 fix: improve JSON-safe rendering for sets 2026-03-06 19:57:08 -06:00
22a1c99abe fix: make empty <select> option work better for WuttaDictEnum
pretty sure this makes sense.. guess we'll see if it causes problems
2026-03-05 20:28:47 -06:00
8 changed files with 176 additions and 21 deletions

View file

@ -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.29.2 (2026-03-10)
### Fix
- add base class for `WuttaList` schema type
- skip custom rendering for missing grid columns
- serialize None to null, for ObjectRef schema type
- improve JSON-safe rendering for sets
- make empty <select> option work better for WuttaDictEnum
## v0.29.1 (2026-03-04)
### Fix

View file

@ -6,7 +6,7 @@ build-backend = "hatchling.build"
[project]
name = "WuttaWeb"
version = "0.29.1"
version = "0.29.2"
description = "Web App for Wutta Framework"
readme = "README.md"
authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]

View file

@ -205,17 +205,23 @@ class WuttaDictEnum(colander.String):
"""
def __init__(self, request, enum_dct, *args, **kwargs):
self.null_value = kwargs.pop("null_value", "")
super().__init__(*args, **kwargs)
self.request = request
self.config = self.request.wutta_config
self.app = self.config.get_app()
self.enum_dct = enum_dct
def serialize(self, node, appstruct):
if appstruct is colander.null:
return self.null_value
return super().serialize(node, appstruct)
def widget_maker(self, **kwargs): # pylint: disable=empty-docstring
""" """
if "values" not in kwargs:
kwargs["values"] = list(self.enum_dct.items())
kwargs.setdefault("null_value", self.null_value)
return widgets.SelectWidget(**kwargs)
@ -274,13 +280,59 @@ class WuttaQuantity(colander.Decimal):
return self.app.render_quantity(appstruct)
class WuttaSet(colander.Set):
class WuttaList(colander.List):
"""
Custom schema type for :class:`python:set` fields.
This is a subclass of :class:`colander.Set`.
Custom schema type for :class:`python:list` fields; this is a
subclass of :class:`colander.List`.
:param request: Current :term:`request` object.
As of now this merely provides a way (in fact, requires you) to
pass the request in, so it can be leveraged as needed. Instances
of this type will have the following attributes:
.. attribute:: request
Reference to the current :term:`request`.
.. attribute:: config
Reference to the app :term:`config object`.
.. attribute:: app
Reference to the :term:`app handler` instance.
"""
def __init__(self, request):
super().__init__()
self.request = request
self.config = self.request.wutta_config
self.app = self.config.get_app()
class WuttaSet(colander.Set):
"""
Custom schema type for :class:`python:set` fields; this is a
subclass of :class:`colander.Set`.
:param request: Current :term:`request` object.
As of now this merely provides a way (in fact, requires you) to
pass the request in, so it can be leveraged as needed. Instances
of this type will have the following attributes:
.. attribute:: request
Reference to the current :term:`request`.
.. attribute:: config
Reference to the app :term:`config object`.
.. attribute:: app
Reference to the :term:`app handler` instance.
"""
def __init__(self, request):
@ -351,15 +403,17 @@ class ObjectRef(colander.SchemaType):
def serialize(self, node, appstruct): # pylint: disable=empty-docstring
""" """
# nb. normalize to empty option if no object ref, so that
# works as expected
# normalize to empty option if no object ref, so that works as
# expected
if self.empty_option and not appstruct:
return self.empty_option[0]
if appstruct is colander.null:
# even if there is no empty option, still treat any false-ish
# value as null
if not appstruct:
return colander.null
# nb. keep a ref to this for later use
# keep a ref to this for later use
node.model_instance = appstruct
# serialize to PK as string

View file

@ -2515,6 +2515,8 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
# customize value rendering where applicable
for key, renderer in self.renderers.items():
# nb. no need to render if column not included
if key in self.columns:
value = record.get(key, None)
record[key] = renderer(original_record, key, value)

View file

@ -654,6 +654,13 @@ def make_json_safe(value, key=None, warn=True):
parent[i] = make_json_safe(v, key=key, warn=warn)
value = parent
elif isinstance(value, set):
# recursively convert set (as list)
parent = list(value)
for i, v in enumerate(parent):
parent[i] = make_json_safe(v, key=key, warn=warn)
value = parent
elif isinstance(value, _uuid.UUID):
# convert UUID to str
value = value.hex

View file

@ -153,6 +153,20 @@ MOCK_STATUS = {
class TestWuttaDictEnum(WebTestCase):
def test_serialize(self):
# null_value is empty string by default
typ = mod.WuttaDictEnum(self.request, MOCK_STATUS)
node = colander.SchemaNode(typ)
self.assertEqual(typ.serialize(node, 1), "1")
self.assertEqual(typ.serialize(node, colander.null), "")
# but can override if needed (?)
typ = mod.WuttaDictEnum(self.request, MOCK_STATUS, null_value="nope")
node = colander.SchemaNode(typ)
self.assertEqual(typ.serialize(node, 1), "1")
self.assertEqual(typ.serialize(node, colander.null), "nope")
def test_widget_maker(self):
typ = mod.WuttaDictEnum(self.request, MOCK_STATUS)
widget = typ.widget_maker()
@ -184,6 +198,26 @@ class TestWuttaMoney(WebTestCase):
self.assertEqual(widget.scale, 4)
class TestWuttaList(WebTestCase):
def test_constructor(self):
node = colander.SchemaNode(mod.WuttaList(self.request))
typ = node.typ
self.assertIs(typ.request, self.request)
self.assertIs(typ.config, self.config)
self.assertIs(typ.app, self.app)
class TestWuttaSet(WebTestCase):
def test_constructor(self):
node = colander.SchemaNode(mod.WuttaSet(self.request))
typ = node.typ
self.assertIs(typ.request, self.request)
self.assertIs(typ.config, self.config)
self.assertIs(typ.app, self.app)
class TestWuttaQuantity(WebTestCase):
def test_serialize(self):
@ -243,6 +277,8 @@ class TestObjectRef(DataTestCase):
typ = mod.ObjectRef(self.request)
value = typ.serialize(node, colander.null)
self.assertIs(value, colander.null)
value = typ.serialize(node, None)
self.assertIs(value, colander.null)
# model instance
person = model.Person(full_name="Betty Boop")

View file

@ -1985,11 +1985,13 @@ class TestGrid(WebTestCase):
# typical data is a list
mydata = [
{"foo": "bar"},
{"foo": "bar", "baz": "zoo"},
]
grid = self.make_grid(columns=["foo"], data=mydata)
grid = self.make_grid(columns=["foo", "baz"], data=mydata)
context = grid.get_vue_context()
self.assertEqual(context, {"data": [{"foo": "bar"}], "row_classes": {}})
self.assertEqual(
context, {"data": [{"foo": "bar", "baz": "zoo"}], "row_classes": {}}
)
# non-declared columns are discarded
mydata = [
@ -2001,27 +2003,47 @@ class TestGrid(WebTestCase):
# if grid has actions, that list may be supplemented
mydata = [
{"foo": "bar"},
{"foo": "bar", "baz": "zoo"},
]
grid = self.make_grid(columns=["foo"], data=mydata)
grid = self.make_grid(columns=["foo", "baz"], data=mydata)
grid.actions.append(mod.GridAction(self.request, "view", url="/blarg"))
context = grid.get_vue_context()
self.assertIsNot(context["data"], mydata)
self.assertEqual(
context,
{"data": [{"foo": "bar", "_action_url_view": "/blarg"}], "row_classes": {}},
{
"data": [{"foo": "bar", "baz": "zoo", "_action_url_view": "/blarg"}],
"row_classes": {},
},
)
# can override value rendering
grid.set_renderer("foo", lambda record, key, value: "blah blah")
renderer = MagicMock(return_value="blah blah")
grid.set_renderer("foo", renderer)
context = grid.get_vue_context()
self.assertEqual(
context,
{
"data": [{"foo": "blah blah", "_action_url_view": "/blarg"}],
"data": [
{"foo": "blah blah", "baz": "zoo", "_action_url_view": "/blarg"}
],
"row_classes": {},
},
)
renderer.assert_called_once_with({"foo": "bar", "baz": "zoo"}, "foo", "bar")
# custom rendering skipped if column not included
grid.remove("foo")
renderer.reset_mock()
context = grid.get_vue_context()
self.assertEqual(
context,
{
"data": [{"baz": "zoo", "_action_url_view": "/blarg"}],
"row_classes": {},
},
)
renderer.assert_not_called()
# can set row class
grid.row_class = "whatever"
@ -2029,7 +2051,7 @@ class TestGrid(WebTestCase):
self.assertEqual(
context,
{
"data": [{"foo": "blah blah", "_action_url_view": "/blarg"}],
"data": [{"baz": "zoo", "_action_url_view": "/blarg"}],
"row_classes": {"0": "whatever"},
},
)

View file

@ -735,6 +735,30 @@ class TestMakeJsonSafe(TestCase):
],
)
def test_set(self):
model = self.app.model
person = model.Person(full_name="Betty Boop")
data = {
"foo",
"bar",
person,
}
self.assertRaises(TypeError, json.dumps, data)
value = mod.make_json_safe(data)
self.assertNotIsInstance(value, set)
self.assertIsInstance(value, list)
# nb. must sort values, otherwise comparison may fail
self.assertEqual(
sorted(value),
[
"Betty Boop", # nb. upper-case sorts first
"bar",
"foo",
],
)
class TestRenderVueFinalize(TestCase):