3
0
Fork 0

feat: add support for association proxy fields in model forms

This commit is contained in:
Lance Edgar 2026-02-17 14:06:38 -06:00
parent e1122caa9c
commit 48f96220b6
4 changed files with 338 additions and 4 deletions

View file

@ -4,11 +4,16 @@ import os
from unittest.mock import MagicMock, patch
import sqlalchemy as sa
from sqlalchemy import orm
from sqlalchemy.ext.associationproxy import AssociationProxy
import colander
import deform
import colander
from colanderalchemy import SQLAlchemySchemaNode
from pyramid.renderers import get_renderer
from wuttjamaican.testing import DataTestCase
from wuttaweb.forms import base as mod, widgets
from wuttaweb.grids import Grid
from wuttaweb.testing import WebTestCase
@ -734,3 +739,174 @@ class TestForm(WebTestCase):
self.assertNotIn("foo", dform)
self.assertIn("bar", schema)
self.assertIn("bar", dform)
class TestWuttaSchemaNode(WebTestCase):
def test_add_nodes(self):
model = self.app.model
# ColanderAlchemy does not add nodes for association proxy fields
schema = SQLAlchemySchemaNode(
model.User, includes=["username", "first_name", "last_name"]
)
self.assertIn("username", schema)
self.assertNotIn("first_name", schema)
self.assertNotIn("last_name", schema)
# but our custom class handles them okay
schema = mod.WuttaSchemaNode(
model.User, includes=["username", "first_name", "last_name"]
)
self.assertIn("username", schema)
self.assertIn("first_name", schema)
self.assertIn("last_name", schema)
def test_dictify(self):
model = self.app.model
person = model.Person(first_name="Fred", last_name="Flintstone")
user = model.User(username="freddie", person=person)
user.foo_field = "bar"
foo_field = colander.SchemaNode(colander.String())
# ColanderAlchemy does not include association proxy fields
schema = SQLAlchemySchemaNode(
model.User, includes=["username", "first_name", "last_name", foo_field]
)
dct = schema.dictify(user)
self.assertIn("username", dct)
self.assertEqual(dct["username"], "freddie")
self.assertNotIn("first_name", dct)
self.assertNotIn("last_name", dct)
self.assertNotIn("foo_field", dct)
# but our custom class handles them okay
schema = mod.WuttaSchemaNode(
model.User, includes=["username", "first_name", "last_name", foo_field]
)
dct = schema.dictify(user)
self.assertIn("username", dct)
self.assertEqual(dct["username"], "freddie")
self.assertIn("first_name", dct)
self.assertEqual(dct["first_name"], "Fred")
self.assertIn("last_name", dct)
self.assertEqual(dct["last_name"], "Flintstone")
# nb. foo_field is not assn proxy, so not auto-dictified
self.assertNotIn("foo_field", dct)
# also note special handling for null values on string proxy fields
user.last_name = None
dct = schema.dictify(user)
self.assertIn("first_name", dct)
self.assertIs(dct["first_name"], "Fred")
self.assertIn("last_name", dct)
self.assertIsNotNone(dct["last_name"])
self.assertIs(dct["last_name"], colander.null)
def test_objectify(self):
model = self.app.model
# ColanderAlchemy does not set association proxy fields
schema = SQLAlchemySchemaNode(
model.User,
includes=["username", "first_name", "last_name"],
)
user = schema.objectify(
{
"username": "freddie",
"first_name": "Fred",
"last_name": "Flintstone",
}
)
self.assertIsInstance(user, model.User)
self.assertEqual(user.username, "freddie")
self.assertIsNone(user.person)
self.assertIsNone(user.first_name)
self.assertIsNone(user.last_name)
# but our custom class handles them okay
schema = mod.WuttaSchemaNode(
model.User,
includes=["username", "first_name", "last_name"],
)
user = schema.objectify(
{
"username": "freddie",
"first_name": "Fred",
"last_name": "Flintstone",
}
)
self.assertIsInstance(user, model.User)
self.assertEqual(user.username, "freddie")
self.assertIsInstance(user.person, model.Person)
self.assertEqual(user.person.first_name, "Fred")
self.assertEqual(user.person.last_name, "Flintstone")
self.assertEqual(user.first_name, "Fred")
self.assertEqual(user.last_name, "Flintstone")
# also note special handling for null values on string proxy fields
user = schema.objectify(
{
"username": "freddie",
"first_name": "Fred",
"last_name": colander.null,
}
)
self.assertIsInstance(user, model.User)
self.assertEqual(user.username, "freddie")
self.assertIsInstance(user.person, model.Person)
self.assertEqual(user.person.first_name, "Fred")
self.assertIsNone(user.person.last_name)
self.assertEqual(user.first_name, "Fred")
self.assertIsNone(user.last_name)
class TestGetAssociationProxy(DataTestCase):
def test_basic(self):
model = self.app.model
mapper = sa.inspect(model.User)
# typical
proxy = mod.get_association_proxy(mapper, "first_name")
self.assertIsInstance(proxy, AssociationProxy)
self.assertEqual(proxy.target_collection, "person")
self.assertEqual(proxy.value_attr, "first_name")
# no such proxy
proxy = mod.get_association_proxy(mapper, "blarg")
self.assertIsNone(proxy)
class TestGetAssociationProxyTarget(DataTestCase):
def test_basic(self):
model = self.app.model
mapper = sa.inspect(model.User)
# typical
target = mod.get_association_proxy_target(mapper, "first_name")
self.assertIsInstance(target, orm.RelationshipProperty)
self.assertIs(target, mapper.get_property("person"))
# no such proxy
target = mod.get_association_proxy_target(mapper, "blarg")
self.assertIsNone(target)
class TestGetAssociationProxyColumn(DataTestCase):
def test_basic(self):
model = self.app.model
mapper = sa.inspect(model.User)
# typical
column = mod.get_association_proxy_column(mapper, "first_name")
self.assertIsInstance(column, orm.ColumnProperty)
self.assertIs(column, sa.inspect(model.Person).get_property("first_name"))
# no such proxy
column = mod.get_association_proxy_column(mapper, "blarg")
self.assertIsNone(column)