diff --git a/src/wuttaweb/templates/deform/checkbox.pt b/src/wuttaweb/templates/deform/checkbox.pt
new file mode 100644
index 0000000..92c9f62
--- /dev/null
+++ b/src/wuttaweb/templates/deform/checkbox.pt
@@ -0,0 +1,11 @@
+
+
+ {{ ${vmodel} }}
+
+
diff --git a/src/wuttaweb/templates/deform/checked_password.pt b/src/wuttaweb/templates/deform/checked_password.pt
index 624f8a8..8a009df 100644
--- a/src/wuttaweb/templates/deform/checked_password.pt
+++ b/src/wuttaweb/templates/deform/checked_password.pt
@@ -1,5 +1,6 @@
+ oid oid|field.oid;
+ vmodel vmodel|'modelData.'+oid;">
${field.start_mapping()}
+ oid oid|field.oid;
+ vmodel vmodel|'modelData.'+oid;">
+
+
+
+
+
+
+
+
+
+
diff --git a/src/wuttaweb/templates/deform/textinput.pt b/src/wuttaweb/templates/deform/textinput.pt
index 4f09fc6..89c8c0f 100644
--- a/src/wuttaweb/templates/deform/textinput.pt
+++ b/src/wuttaweb/templates/deform/textinput.pt
@@ -1,6 +1,7 @@
+ oid oid|field.oid;
+ vmodel vmodel|'modelData.'+oid;">
diff --git a/src/wuttaweb/templates/forms/vue_template.mako b/src/wuttaweb/templates/forms/vue_template.mako
index 47e9fc3..70540d0 100644
--- a/src/wuttaweb/templates/forms/vue_template.mako
+++ b/src/wuttaweb/templates/forms/vue_template.mako
@@ -56,12 +56,7 @@
% if not form.readonly:
- ## field model values
- % for key in form:
- % if key in dform:
- model_${key}: ${form.get_vue_field_value(key)|n},
- % endif
- % endfor
+ modelData: ${json.dumps(model_data)|n},
% if form.auto_disable_submit:
formSubmitting: false,
diff --git a/src/wuttaweb/util.py b/src/wuttaweb/util.py
index 4c09478..36942c3 100644
--- a/src/wuttaweb/util.py
+++ b/src/wuttaweb/util.py
@@ -25,10 +25,62 @@ Web Utilities
"""
import importlib
+import json
+import logging
+import colander
from webhelpers2.html import HTML, tags
+log = logging.getLogger(__name__)
+
+
+class FieldList(list):
+ """
+ Convenience wrapper for a form's field list. This is a subclass
+ of :class:`python:list`.
+
+ You normally would not need to instantiate this yourself, but it
+ is used under the hood for
+ :attr:`~wuttaweb.forms.base.Form.fields` as well as
+ :attr:`~wuttaweb.grids.base.Grid.columns`.
+ """
+
+ def insert_before(self, field, newfield):
+ """
+ Insert a new field, before an existing field.
+
+ :param field: String name for the existing field.
+
+ :param newfield: String name for the new field, to be inserted
+ just before the existing ``field``.
+ """
+ if field in self:
+ i = self.index(field)
+ self.insert(i, newfield)
+ else:
+ log.warning("field '%s' not found, will append new field: %s",
+ field, newfield)
+ self.append(newfield)
+
+ def insert_after(self, field, newfield):
+ """
+ Insert a new field, after an existing field.
+
+ :param field: String name for the existing field.
+
+ :param newfield: String name for the new field, to be inserted
+ just after the existing ``field``.
+ """
+ if field in self:
+ i = self.index(field)
+ self.insert(i + 1, newfield)
+ else:
+ log.warning("field '%s' not found, will append new field: %s",
+ field, newfield)
+ self.append(newfield)
+
+
def get_form_data(request):
"""
Returns the effective form data for the given request.
@@ -376,3 +428,47 @@ def get_model_fields(config, model_class=None):
mapper = sa.inspect(model_class)
fields = list([prop.key for prop in mapper.iterate_properties])
return fields
+
+
+def make_json_safe(value, key=None, warn=True):
+ """
+ Convert a Python value as needed, to ensure it is compatible with
+ :func:`python:json.dumps()`.
+
+ :param value: Python value.
+
+ :param key: Optional key for the value, if known. This is used
+ when logging warnings, if applicable.
+
+ :param warn: Whether warnings should be logged if the value is not
+ already JSON-compatible.
+
+ :returns: A (possibly new) Python value which is guaranteed to be
+ JSON-serializable.
+ """
+
+ # convert null => None
+ if value is colander.null:
+ return None
+
+ # recursively convert dict
+ if isinstance(value, dict):
+ parent = dict(value)
+ for key, value in parent.items():
+ parent[key] = make_json_safe(value, key=key, warn=warn)
+ value = parent
+
+ # ensure JSON-compatibility, warn if problems
+ try:
+ json.dumps(value)
+ except TypeError as error:
+ if warn:
+ prefix = "value"
+ if key:
+ prefix += f" for '{key}'"
+ log.warning("%s is not json-friendly: %s", prefix, repr(value))
+ value = str(value)
+ if warn:
+ log.warning("forced value to: %s", value)
+
+ return value
diff --git a/src/wuttaweb/views/essential.py b/src/wuttaweb/views/essential.py
index f004201..2a77df5 100644
--- a/src/wuttaweb/views/essential.py
+++ b/src/wuttaweb/views/essential.py
@@ -33,6 +33,7 @@ That will in turn include the following modules:
* :mod:`wuttaweb.views.common`
* :mod:`wuttaweb.views.settings`
* :mod:`wuttaweb.views.people`
+* :mod:`wuttaweb.views.users`
"""
@@ -43,6 +44,7 @@ def defaults(config, **kwargs):
config.include(mod('wuttaweb.views.common'))
config.include(mod('wuttaweb.views.settings'))
config.include(mod('wuttaweb.views.people'))
+ config.include(mod('wuttaweb.views.users'))
def includeme(config):
diff --git a/src/wuttaweb/views/master.py b/src/wuttaweb/views/master.py
index a735cdd..2cd816a 100644
--- a/src/wuttaweb/views/master.py
+++ b/src/wuttaweb/views/master.py
@@ -1026,9 +1026,11 @@ class MasterView(View):
ready to use as-is, but this method can further modify it
based on request details etc.
"""
+ if 'uuid' in grid.columns:
+ grid.columns.remove('uuid')
+
for key in self.get_model_key():
grid.set_link(key)
- # print("set link:", key)
def get_instance(self, session=None):
"""
@@ -1201,13 +1203,10 @@ class MasterView(View):
already be "complete" and ready to use as-is, but this method
can further modify it based on request details etc.
"""
- model_keys = self.get_model_key()
-
- if 'uuid' in form:
- form.fields.remove('uuid')
+ form.remove('uuid')
if self.editing:
- for key in model_keys:
+ for key in self.get_model_key():
form.set_readonly(key)
def objectify(self, form):
@@ -1228,22 +1227,15 @@ class MasterView(View):
See also :meth:`edit_save_form()` which calls this method.
"""
- model = self.app.model
- model_class = self.get_model_class()
- if model_class and issubclass(model_class, model.Base):
-
- # update instance attrs for sqlalchemy model
- if self.creating:
- obj = model_class()
- else:
- obj = form.model_instance
- data = form.validated
- for key in form.fields:
- if key in data:
- setattr(obj, key, data[key])
- return obj
+ # use ColanderAlchemy magic if possible
+ schema = form.get_schema()
+ if hasattr(schema, 'objectify'):
+ # this returns a model instance
+ return schema.objectify(form.validated,
+ context=form.model_instance)
+ # otherwise return data dict as-is
return form.validated
def persist(self, obj, session=None):
diff --git a/src/wuttaweb/views/users.py b/src/wuttaweb/views/users.py
new file mode 100644
index 0000000..124aa59
--- /dev/null
+++ b/src/wuttaweb/views/users.py
@@ -0,0 +1,117 @@
+# -*- coding: utf-8; -*-
+################################################################################
+#
+# wuttaweb -- Web App for Wutta Framework
+# Copyright © 2024 Lance Edgar
+#
+# This file is part of Wutta Framework.
+#
+# Wutta Framework is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option) any
+# later version.
+#
+# Wutta Framework is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# Wutta Framework. If not, see .
+#
+################################################################################
+"""
+Views for users
+"""
+
+import colander
+
+from wuttjamaican.db.model import User
+from wuttaweb.views import MasterView
+from wuttaweb.forms.schema import PersonRef
+from wuttaweb.db import Session
+
+
+class UserView(MasterView):
+ """
+ Master view for users.
+
+ Notable URLs provided by this class:
+
+ * ``/users/``
+ * ``/users/new``
+ * ``/users/XXX``
+ * ``/users/XXX/edit``
+ * ``/users/XXX/delete``
+ """
+ model_class = User
+
+ grid_columns = [
+ 'username',
+ 'person',
+ 'active',
+ ]
+
+ # TODO: master should handle this, possibly via configure_form()
+ def get_query(self, session=None):
+ """ """
+ model = self.app.model
+ query = super().get_query(session=session)
+ return query.order_by(model.User.username)
+
+ def configure_grid(self, g):
+ """ """
+ super().configure_grid(g)
+
+ # never show these
+ g.remove('person_uuid',
+ 'role_refs',
+ 'password')
+
+ # username
+ g.set_link('username')
+
+ # person
+ g.set_link('person')
+
+ def configure_form(self, f):
+ """ """
+ super().configure_form(f)
+
+ # never show these
+ f.remove('person_uuid',
+ 'password',
+ 'role_refs')
+
+ # person
+ f.set_node('person', PersonRef(self.request, empty_option=True))
+ f.set_required('person', False)
+
+ # username
+ f.set_validator('username', self.unique_username)
+
+ def unique_username(self, node, value):
+ """ """
+ model = self.app.model
+ session = Session()
+
+ query = session.query(model.User)\
+ .filter(model.User.username == value)
+
+ if self.editing:
+ uuid = self.request.matchdict['uuid']
+ query = query.filter(model.User.uuid != uuid)
+
+ if query.count():
+ node.raise_invalid("Username must be unique")
+
+
+def defaults(config, **kwargs):
+ base = globals()
+
+ UserView = kwargs.get('UserView', base['UserView'])
+ UserView.defaults(config)
+
+
+def includeme(config):
+ defaults(config)
diff --git a/tests/forms/test_base.py b/tests/forms/test_base.py
index 0b579b6..b3aade2 100644
--- a/tests/forms/test_base.py
+++ b/tests/forms/test_base.py
@@ -8,46 +8,15 @@ import deform
from pyramid import testing
from wuttjamaican.conf import WuttaConfig
-from wuttaweb.forms import base
+from wuttaweb.forms import base, widgets
from wuttaweb import helpers
-class TestFieldList(TestCase):
-
- def test_insert_before(self):
- fields = base.FieldList(['f1', 'f2'])
- self.assertEqual(fields, ['f1', 'f2'])
-
- # typical
- fields.insert_before('f1', 'XXX')
- self.assertEqual(fields, ['XXX', 'f1', 'f2'])
- fields.insert_before('f2', 'YYY')
- self.assertEqual(fields, ['XXX', 'f1', 'YYY', 'f2'])
-
- # appends new field if reference field is invalid
- fields.insert_before('f3', 'ZZZ')
- self.assertEqual(fields, ['XXX', 'f1', 'YYY', 'f2', 'ZZZ'])
-
- def test_insert_after(self):
- fields = base.FieldList(['f1', 'f2'])
- self.assertEqual(fields, ['f1', 'f2'])
-
- # typical
- fields.insert_after('f1', 'XXX')
- self.assertEqual(fields, ['f1', 'XXX', 'f2'])
- fields.insert_after('XXX', 'YYY')
- self.assertEqual(fields, ['f1', 'XXX', 'YYY', 'f2'])
-
- # appends new field if reference field is invalid
- fields.insert_after('f3', 'ZZZ')
- self.assertEqual(fields, ['f1', 'XXX', 'YYY', 'f2', 'ZZZ'])
-
-
class TestForm(TestCase):
def setUp(self):
self.config = WuttaConfig(defaults={
- 'wutta.web.menus.handler_spec': 'tests.utils:NullMenuHandler',
+ 'wutta.web.menus.handler_spec': 'tests.util:NullMenuHandler',
})
self.app = self.config.get_app()
self.request = testing.DummyRequest(wutta_config=self.config, use_oruga=False)
@@ -74,7 +43,7 @@ class TestForm(TestCase):
def test_init_with_none(self):
form = self.make_form()
- self.assertIsNone(form.fields)
+ self.assertEqual(form.fields, [])
def test_init_with_fields(self):
form = self.make_form(fields=['foo', 'bar'])
@@ -115,6 +84,79 @@ class TestForm(TestCase):
form.set_fields(['baz'])
self.assertEqual(form.fields, ['baz'])
+ def test_remove(self):
+ form = self.make_form(fields=['one', 'two', 'three', 'four'])
+ self.assertEqual(form.fields, ['one', 'two', 'three', 'four'])
+ form.remove('two', 'three')
+ self.assertEqual(form.fields, ['one', 'four'])
+
+ def test_set_node(self):
+ form = self.make_form(fields=['foo', 'bar'])
+ self.assertEqual(form.nodes, {})
+
+ # complete node
+ node = colander.SchemaNode(colander.Bool(), name='foo')
+ form.set_node('foo', node)
+ self.assertIs(form.nodes['foo'], node)
+
+ # type only
+ typ = colander.Bool()
+ form.set_node('foo', typ)
+ node = form.nodes['foo']
+ self.assertIsInstance(node, colander.SchemaNode)
+ self.assertIsInstance(node.typ, colander.Bool)
+ self.assertEqual(node.name, 'foo')
+
+ # schema is updated if already present
+ schema = form.get_schema()
+ self.assertIsNotNone(schema)
+ typ = colander.Date()
+ form.set_node('foo', typ)
+ node = form.nodes['foo']
+ self.assertIsInstance(node, colander.SchemaNode)
+ self.assertIsInstance(node.typ, colander.Date)
+ self.assertEqual(node.name, 'foo')
+
+ def test_set_widget(self):
+ form = self.make_form(fields=['foo', 'bar'])
+ self.assertEqual(form.widgets, {})
+
+ # basic
+ widget = widgets.SelectWidget()
+ form.set_widget('foo', widget)
+ self.assertIs(form.widgets['foo'], widget)
+
+ # schema is updated if already present
+ schema = form.get_schema()
+ self.assertIsNotNone(schema)
+ self.assertIs(schema['foo'].widget, widget)
+ new_widget = widgets.TextInputWidget()
+ form.set_widget('foo', new_widget)
+ self.assertIs(form.widgets['foo'], new_widget)
+ self.assertIs(schema['foo'].widget, new_widget)
+
+ def test_set_validator(self):
+ form = self.make_form(fields=['foo', 'bar'])
+ self.assertEqual(form.validators, {})
+
+ def validate1(node, value):
+ pass
+
+ # basic
+ form.set_validator('foo', validate1)
+ self.assertIs(form.validators['foo'], validate1)
+
+ def validate2(node, value):
+ pass
+
+ # schema is updated if already present
+ schema = form.get_schema()
+ self.assertIsNotNone(schema)
+ self.assertIs(schema['foo'].validator, validate1)
+ form.set_validator('foo', validate2)
+ self.assertIs(form.validators['foo'], validate2)
+ self.assertIs(schema['foo'].validator, validate2)
+
def test_get_schema(self):
model = self.app.model
form = self.make_form()
@@ -144,6 +186,13 @@ class TestForm(TestCase):
self.assertIn('name', schema)
self.assertIn('value', schema)
+ # but node overrides are honored when auto-generating
+ form = self.make_form(model_class=model.Setting)
+ value_node = colander.SchemaNode(colander.Bool(), name='value')
+ form.set_node('value', value_node)
+ schema = form.get_schema()
+ self.assertIs(schema['value'], value_node)
+
# schema is auto-generated if model_instance provided
form = self.make_form(model_instance=model.Setting(name='uhoh'))
self.assertEqual(form.fields, ['name', 'value'])
@@ -166,7 +215,23 @@ class TestForm(TestCase):
form.set_required('bar', False)
schema = form.get_schema()
self.assertIs(schema['foo'].missing, colander.required)
- self.assertIsNone(schema['bar'].missing)
+ self.assertIs(schema['bar'].missing, colander.null)
+
+ # validator overrides are honored
+ def validate(node, value): pass
+ form = self.make_form(model_class=model.Setting)
+ form.set_validator('name', validate)
+ schema = form.get_schema()
+ self.assertIs(schema['name'].validator, validate)
+
+ # validator can be set for whole form
+ form = self.make_form(model_class=model.Setting)
+ schema = form.get_schema()
+ self.assertIsNone(schema.validator)
+ form = self.make_form(model_class=model.Setting)
+ form.set_validator(None, validate)
+ schema = form.get_schema()
+ self.assertIs(schema.validator, validate)
def test_get_deform(self):
model = self.app.model
@@ -372,34 +437,6 @@ class TestForm(TestCase):
self.assertEqual(len(errors), 1)
self.assertEqual(errors[0], "something is wrong")
- def test_get_vue_field_value(self):
- schema = self.make_schema()
- form = self.make_form(schema=schema)
-
- # null field value
- value = form.get_vue_field_value('foo')
- self.assertEqual(value, 'null')
-
- # non-default / explicit value
- # TODO: surely need a different approach to set value
- dform = form.get_deform()
- dform['foo'].cstruct = 'blarg'
- value = form.get_vue_field_value('foo')
- self.assertEqual(value, '"blarg"')
-
- def test_jsonify_value(self):
- form = self.make_form()
-
- # null field value
- value = form.jsonify_value(colander.null)
- self.assertEqual(value, 'null')
- value = form.jsonify_value(None)
- self.assertEqual(value, 'null')
-
- # string value
- value = form.jsonify_value('blarg')
- self.assertEqual(value, '"blarg"')
-
def test_validate(self):
schema = self.make_schema()
form = self.make_form(schema=schema)
diff --git a/tests/forms/test_schema.py b/tests/forms/test_schema.py
new file mode 100644
index 0000000..3d2492b
--- /dev/null
+++ b/tests/forms/test_schema.py
@@ -0,0 +1,199 @@
+# -*- coding: utf-8; -*-
+
+from unittest.mock import patch
+
+import colander
+from pyramid import testing
+
+from sqlalchemy import orm
+
+from wuttjamaican.conf import WuttaConfig
+from wuttaweb.forms import schema as mod
+from tests.util import DataTestCase
+
+
+class TestObjectNode(DataTestCase):
+
+ def setUp(self):
+ self.setup_db()
+ self.request = testing.DummyRequest(wutta_config=self.config)
+
+ def test_dictify(self):
+ model = self.app.model
+ person = model.Person(full_name="Betty Boop")
+
+ # unsupported type raises error
+ node = mod.ObjectNode(colander.String())
+ self.assertRaises(NotImplementedError, node.dictify, person)
+
+ # but supported type can dictify
+ node = mod.ObjectNode(mod.PersonRef(self.request))
+ value = node.dictify(person)
+ self.assertIs(value, person)
+
+ def test_objectify(self):
+ model = self.app.model
+ person = model.Person(full_name="Betty Boop")
+
+ # unsupported type raises error
+ node = mod.ObjectNode(colander.String())
+ self.assertRaises(NotImplementedError, node.objectify, person)
+
+ # but supported type can objectify
+ node = mod.ObjectNode(mod.PersonRef(self.request))
+ value = node.objectify(person)
+ self.assertIs(value, person)
+
+
+class TestObjectRef(DataTestCase):
+
+ def setUp(self):
+ self.setup_db()
+ self.request = testing.DummyRequest(wutta_config=self.config)
+
+ def test_empty_option(self):
+
+ # null by default
+ typ = mod.ObjectRef(self.request)
+ self.assertIsNone(typ.empty_option)
+
+ # passing true yields default empty option
+ typ = mod.ObjectRef(self.request, empty_option=True)
+ self.assertEqual(typ.empty_option, ('', "(none)"))
+
+ # can set explicitly
+ typ = mod.ObjectRef(self.request, empty_option=('foo', 'bar'))
+ self.assertEqual(typ.empty_option, ('foo', 'bar'))
+
+ # can set just a label
+ typ = mod.ObjectRef(self.request, empty_option="(empty)")
+ self.assertEqual(typ.empty_option, ('', "(empty)"))
+
+ def test_model_class(self):
+ typ = mod.ObjectRef(self.request)
+ self.assertRaises(NotImplementedError, getattr, typ, 'model_class')
+
+ def test_serialize(self):
+ model = self.app.model
+ node = colander.SchemaNode(colander.String())
+
+ # null
+ typ = mod.ObjectRef(self.request)
+ value = typ.serialize(node, colander.null)
+ self.assertIs(value, colander.null)
+
+ # model instance
+ person = model.Person(full_name="Betty Boop")
+ self.session.add(person)
+ self.session.commit()
+ self.assertIsNotNone(person.uuid)
+ typ = mod.ObjectRef(self.request)
+ value = typ.serialize(node, person)
+ self.assertEqual(value, person.uuid)
+
+ def test_deserialize(self):
+ model = self.app.model
+ node = colander.SchemaNode(colander.String())
+
+ # null
+ typ = mod.ObjectRef(self.request)
+ value = typ.deserialize(node, colander.null)
+ self.assertIs(value, colander.null)
+
+ # model instance
+ person = model.Person(full_name="Betty Boop")
+ self.session.add(person)
+ self.session.commit()
+ self.assertIsNotNone(person.uuid)
+ with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
+ typ = mod.ObjectRef(self.request, session=self.session)
+ value = typ.deserialize(node, person.uuid)
+ self.assertIs(value, person)
+
+ def test_dictify(self):
+ model = self.app.model
+ node = colander.SchemaNode(colander.String())
+
+ # model instance
+ person = model.Person(full_name="Betty Boop")
+ self.session.add(person)
+ self.session.commit()
+ self.assertIsNotNone(person.uuid)
+ typ = mod.ObjectRef(self.request)
+ value = typ.dictify(person)
+ self.assertIs(value, person)
+
+ def test_objectify(self):
+ model = self.app.model
+ node = colander.SchemaNode(colander.String())
+
+ # null
+ typ = mod.ObjectRef(self.request)
+ value = typ.objectify(None)
+ self.assertIsNone(value)
+
+ # model instance
+ person = model.Person(full_name="Betty Boop")
+ self.session.add(person)
+ self.session.commit()
+ self.assertIsNotNone(person.uuid)
+ with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
+ typ = mod.ObjectRef(self.request, session=self.session)
+ value = typ.objectify(person.uuid)
+ self.assertIs(value, person)
+
+ # error if not found
+ with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
+ typ = mod.ObjectRef(self.request, session=self.session)
+ self.assertRaises(ValueError, typ.objectify, 'WRONG-UUID')
+
+ def test_get_query(self):
+ model = self.app.model
+ with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
+ typ = mod.ObjectRef(self.request, session=self.session)
+ query = typ.get_query()
+ self.assertIsInstance(query, orm.Query)
+
+ def test_sort_query(self):
+ model = self.app.model
+ with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
+ typ = mod.ObjectRef(self.request, session=self.session)
+ query = typ.get_query()
+ sorted_query = typ.sort_query(query)
+ self.assertIs(sorted_query, query)
+
+ def test_widget_maker(self):
+ model = self.app.model
+ person = model.Person(full_name="Betty Boop")
+ self.session.add(person)
+ self.session.commit()
+
+ # basic
+ with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
+ typ = mod.ObjectRef(self.request, session=self.session)
+ widget = typ.widget_maker()
+ self.assertEqual(len(widget.values), 1)
+ self.assertEqual(widget.values[0][1], "Betty Boop")
+
+ # empty option
+ with patch.object(mod.ObjectRef, 'model_class', new=model.Person):
+ typ = mod.ObjectRef(self.request, session=self.session, empty_option=True)
+ widget = typ.widget_maker()
+ self.assertEqual(len(widget.values), 2)
+ self.assertEqual(widget.values[0][1], "(none)")
+ self.assertEqual(widget.values[1][1], "Betty Boop")
+
+
+class TestPersonRef(DataTestCase):
+
+ def setUp(self):
+ self.setup_db()
+ self.request = testing.DummyRequest(wutta_config=self.config)
+
+ def test_sort_query(self):
+ typ = mod.PersonRef(self.request, session=self.session)
+ query = typ.get_query()
+ self.assertIsInstance(query, orm.Query)
+ sorted_query = typ.sort_query(query)
+ self.assertIsInstance(sorted_query, orm.Query)
+ self.assertIsNot(sorted_query, query)
diff --git a/tests/forms/test_widgets.py b/tests/forms/test_widgets.py
new file mode 100644
index 0000000..d131410
--- /dev/null
+++ b/tests/forms/test_widgets.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8; -*-
+
+import colander
+import deform
+from pyramid import testing
+
+from wuttaweb.forms import widgets
+from wuttaweb.forms.schema import PersonRef
+from tests.util import WebTestCase
+
+class TestObjectRefWidget(WebTestCase):
+
+ def test_serialize(self):
+ model = self.app.model
+ person = model.Person(full_name="Betty Boop")
+ self.session.add(person)
+ self.session.commit()
+
+ # standard (editable)
+ node = colander.SchemaNode(PersonRef(self.request, session=self.session))
+ widget = widgets.ObjectRefWidget(self.request)
+ field = deform.Field(node)
+ html = widget.serialize(field, person.uuid)
+ self.assertIn('