2
0
Fork 0

feat: improve page linkage between role/user/person

- show Users grid when viewing a Role
- add hyperlinks between things
This commit is contained in:
Lance Edgar 2024-08-21 14:38:34 -05:00
parent 9d261de45a
commit 770c4612d5
16 changed files with 440 additions and 21 deletions

View file

@ -25,6 +25,7 @@ Base form classes
""" """
import logging import logging
from collections import OrderedDict
import colander import colander
import deform import deform
@ -311,6 +312,9 @@ class Form:
self.set_fields(fields or self.get_fields()) self.set_fields(fields or self.get_fields())
# nb. this tracks grid JSON data for inclusion in page template
self.grid_vue_data = OrderedDict()
def __contains__(self, name): def __contains__(self, name):
""" """
Custom logic for the ``in`` operator, to allow easily checking Custom logic for the ``in`` operator, to allow easily checking
@ -750,6 +754,10 @@ class Form:
kwargs['appstruct'] = self.model_instance kwargs['appstruct'] = self.model_instance
form = deform.Form(schema, **kwargs) form = deform.Form(schema, **kwargs)
# nb. must give a reference back to wutta form; this is
# for sake of field schema nodes and widgets, e.g. to
# access the main model instance
form.wutta_form = self
self.deform_form = form self.deform_form = form
return self.deform_form return self.deform_form
@ -818,6 +826,17 @@ class Form:
output = render(template, context) output = render(template, context)
return HTML.literal(output) return HTML.literal(output)
def add_grid_vue_data(self, grid):
""" """
if not grid.key:
raise ValueError("grid must have a key!")
if grid.key in self.grid_vue_data:
log.warning("grid data with key '%s' already registered, "
"but will be replaced", grid.key)
self.grid_vue_data[grid.key] = grid.get_vue_data()
def render_vue_field( def render_vue_field(
self, self,
fieldname, fieldname,

View file

@ -246,6 +246,9 @@ class ObjectRef(colander.SchemaType):
values.insert(0, self.empty_option) values.insert(0, self.empty_option)
kwargs['values'] = values kwargs['values'] = values
if 'url' not in kwargs:
kwargs['url'] = lambda person: self.request.route_url('people.view', uuid=person.uuid)
return widgets.ObjectRefWidget(self.request, **kwargs) return widgets.ObjectRefWidget(self.request, **kwargs)
@ -321,6 +324,28 @@ class RoleRefs(WuttaSet):
return widgets.RoleRefsWidget(self.request, **kwargs) return widgets.RoleRefsWidget(self.request, **kwargs)
class UserRefs(WuttaSet):
"""
Form schema type for the Role
:attr:`~wuttjamaican:wuttjamaican.db.model.auth.Role.users`
association proxy field.
This is a subclass of :class:`WuttaSet`. It uses a ``set`` of
:class:`~wuttjamaican:wuttjamaican.db.model.auth.User` ``uuid``
values for underlying data format.
"""
def widget_maker(self, **kwargs):
"""
Constructs a default widget for the field.
:returns: Instance of
:class:`~wuttaweb.forms.widgets.UserRefsWidget`.
"""
kwargs.setdefault('session', self.session)
return widgets.UserRefsWidget(self.request, **kwargs)
class Permissions(WuttaSet): class Permissions(WuttaSet):
""" """
Form schema type for the Role Form schema type for the Role

View file

@ -44,6 +44,7 @@ from deform.widget import (Widget, TextInputWidget, TextAreaWidget,
from webhelpers2.html import HTML from webhelpers2.html import HTML
from wuttaweb.db import Session from wuttaweb.db import Session
from wuttaweb.grids import Grid
class ObjectRefWidget(SelectWidget): class ObjectRefWidget(SelectWidget):
@ -83,9 +84,19 @@ class ObjectRefWidget(SelectWidget):
""" """
readonly_template = 'readonly/objectref' readonly_template = 'readonly/objectref'
def __init__(self, request, *args, **kwargs): def __init__(self, request, url=None, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.request = request self.request = request
self.url = url
def get_template_values(self, field, cstruct, kw):
""" """
values = super().get_template_values(field, cstruct, kw)
if 'url' not in values and self.url and field.schema.model_instance:
values['url'] = self.url(field.schema.model_instance)
return values
class NotesWidget(TextAreaWidget): class NotesWidget(TextAreaWidget):
@ -137,12 +148,17 @@ class RoleRefsWidget(WuttaCheckboxChoiceWidget):
""" """
Widget for use with User Widget for use with User
:attr:`~wuttjamaican:wuttjamaican.db.model.auth.User.roles` field. :attr:`~wuttjamaican:wuttjamaican.db.model.auth.User.roles` field.
This is the default widget for the
:class:`~wuttaweb.forms.schema.RoleRefs` type.
This is a subclass of :class:`WuttaCheckboxChoiceWidget`. This is a subclass of :class:`WuttaCheckboxChoiceWidget`.
""" """
readonly_template = 'readonly/rolerefs'
def serialize(self, field, cstruct, **kw): def serialize(self, field, cstruct, **kw):
""" """ """ """
model = self.app.model
# special logic when field is editable # special logic when field is editable
readonly = kw.get('readonly', self.readonly) readonly = kw.get('readonly', self.readonly)
if not readonly: if not readonly:
@ -159,10 +175,78 @@ class RoleRefsWidget(WuttaCheckboxChoiceWidget):
if val[0] != admin.uuid] if val[0] != admin.uuid]
kw['values'] = values kw['values'] = values
else: # readonly
# roles
roles = []
if cstruct:
for uuid in cstruct:
role = self.session.query(model.Role).get(uuid)
if role:
roles.append(role)
kw['roles'] = roles
# url
url = lambda role: self.request.route_url('roles.view', uuid=role.uuid)
kw['url'] = url
# default logic from here # default logic from here
return super().serialize(field, cstruct, **kw) return super().serialize(field, cstruct, **kw)
class UserRefsWidget(WuttaCheckboxChoiceWidget):
"""
Widget for use with Role
:attr:`~wuttjamaican:wuttjamaican.db.model.auth.Role.users` field.
This is the default widget for the
:class:`~wuttaweb.forms.schema.UserRefs` type.
This is a subclass of :class:`WuttaCheckboxChoiceWidget`; however
it only supports readonly mode and does not use a template.
Rather, it generates and renders a
:class:`~wuttaweb.grids.base.Grid` showing the users list.
"""
def serialize(self, field, cstruct, **kw):
""" """
readonly = kw.get('readonly', self.readonly)
if not readonly:
raise NotImplementedError("edit not allowed for this widget")
model = self.app.model
columns = ['person', 'username', 'active']
# generate data set for users
users = []
if cstruct:
for uuid in cstruct:
user = self.session.query(model.User).get(uuid)
if user:
users.append(dict([(key, getattr(user, key))
for key in columns + ['uuid']]))
# grid
grid = Grid(self.request, key='roles.view.users',
columns=columns, data=users)
# view action
if self.request.has_perm('users.view'):
url = lambda user, i: self.request.route_url('users.view', uuid=user['uuid'])
grid.add_action('view', icon='eye', url=url)
grid.set_link('person')
grid.set_link('username')
# edit action
if self.request.has_perm('users.edit'):
url = lambda user, i: self.request.route_url('users.edit', uuid=user['uuid'])
grid.add_action('edit', url=url)
# render as simple <b-table>
# nb. must indicate we are a part of this form
form = getattr(field.parent, 'wutta_form', None)
return grid.render_table_element(form)
class PermissionsWidget(WuttaCheckboxChoiceWidget): class PermissionsWidget(WuttaCheckboxChoiceWidget):
""" """
Widget for use with Role Widget for use with Role

View file

@ -543,6 +543,13 @@ class Grid:
return True return True
return False return False
def add_action(self, key, **kwargs):
"""
Convenience to add a new :class:`GridAction` instance to the
grid's :attr:`actions` list.
"""
self.actions.append(GridAction(self.request, key, **kwargs))
############################## ##############################
# sorting methods # sorting methods
############################## ##############################
@ -1251,6 +1258,9 @@ class Grid:
""" """
Render the Vue template block for the grid. Render the Vue template block for the grid.
This is what you want for a "full-featured" grid which will
exist as its own unique Vue component on the frontend.
This returns something like: This returns something like:
.. code-block:: none .. code-block:: none
@ -1261,12 +1271,21 @@ class Grid:
</b-table> </b-table>
</script> </script>
<script>
WuttaGridData = {}
WuttaGrid = {
template: 'wutta-grid-template',
}
</script>
.. todo:: .. todo::
Why can't Sphinx render the above code block as 'html' ? Why can't Sphinx render the above code block as 'html' ?
It acts like it can't handle a ``<script>`` tag at all? It acts like it can't handle a ``<script>`` tag at all?
See :meth:`render_table_element()` for a simpler variant.
Actual output will of course depend on grid attributes, Actual output will of course depend on grid attributes,
:attr:`vue_tagname` and :attr:`columns` etc. :attr:`vue_tagname` and :attr:`columns` etc.
@ -1278,6 +1297,58 @@ class Grid:
output = render(template, context) output = render(template, context)
return HTML.literal(output) return HTML.literal(output)
def render_table_element(
self,
form=None,
template='/grids/element.mako',
**context):
"""
Render a simple Vue table element for the grid.
This is what you want for a "simple" grid which does require a
unique Vue component, but can instead use the standard table
component.
This returns something like:
.. code-block:: html
<b-table :data="gridData['mykey']">
<!-- columns etc. -->
</b-table>
See :meth:`render_vue_template()` for a more complete variant.
Actual output will of course depend on grid attributes,
:attr:`key`, :attr:`columns` etc.
:param form: Reference to the
:class:`~wuttaweb.forms.base.Form` instance which
"contains" this grid. This is needed in order to ensure
the grid data is available to the form Vue component.
:param template: Path to Mako template which is used to render
the output.
.. note::
The above example shows ``gridData['mykey']`` as the Vue
data reference. This should "just work" if you provide the
correct ``form`` arg and the grid is contained directly by
that form's Vue component.
However, this may not account for all use cases. For now
we wait and see what comes up, but know the dust may not
yet be settled here.
"""
# nb. must register data for inclusion on page template
if form:
form.add_grid_vue_data(self)
# otherwise logic is the same, just different template
return self.render_vue_template(template=template, **context)
def render_vue_finalize(self): def render_vue_finalize(self):
""" """
Render the Vue "finalize" script for the grid. Render the Vue "finalize" script for the grid.

View file

@ -501,7 +501,7 @@
label="Delete This" /> label="Delete This" />
% endif % endif
% elif master.editing: % elif master.editing:
% if instance_viewable and master.has_perm('view'): % if master.has_perm('view'):
<wutta-button once <wutta-button once
tag="a" href="${master.get_action_url('view', instance)}" tag="a" href="${master.get_action_url('view', instance)}"
icon-left="eye" icon-left="eye"
@ -514,7 +514,7 @@
label="Delete This" /> label="Delete This" />
% endif % endif
% elif master.deleting: % elif master.deleting:
% if instance_viewable and master.has_perm('view'): % if master.has_perm('view'):
<wutta-button once <wutta-button once
tag="a" href="${master.get_action_url('view', instance)}" tag="a" href="${master.get_action_url('view', instance)}"
icon-left="eye" icon-left="eye"

View file

@ -1 +1,9 @@
<span>${str(field.schema.model_instance or '')}</span> <tal:omit tal:define="url url|None;">
<a tal:condition="url"
href="${url}">
${str(field.schema.model_instance or '')}
</a>
<span tal:condition="not url">
${str(field.schema.model_instance or '')}
</span>
</tal:omit>

View file

@ -0,0 +1,7 @@
<ul class="list-group">
<tal:loop tal:repeat="role roles">
<li class="list-group-item">
<a href="${url(role)}">${role}</a>
</li>
</tal:loop>
</ul>

View file

@ -68,6 +68,14 @@
% endif % endif
% endif % endif
% if form.grid_vue_data:
gridData: {
% for key, data in form.grid_vue_data.items():
'${key}': ${json.dumps(data)|n},
% endfor
},
% endif
} }
</script> </script>

View file

@ -0,0 +1,49 @@
## -*- coding: utf-8; -*-
<${b}-table :data="gridData['${grid.key}']">
% for column in grid.get_vue_columns():
<${b}-table-column field="${column['field']}"
label="${column['label']}"
v-slot="props"
:sortable="${json.dumps(column.get('sortable', False))|n}"
cell-class="c_${column['field']}">
% if grid.is_linked(column['field']):
<a :href="props.row._action_url_view"
v-html="props.row.${column['field']}" />
% else:
<span v-html="props.row.${column['field']}"></span>
% endif
</${b}-table-column>
% endfor
% if grid.actions:
<${b}-table-column field="actions"
label="Actions"
v-slot="props">
% for action in grid.actions:
<a v-if="props.row._action_url_${action.key}"
:href="props.row._action_url_${action.key}"
class="${action.link_class}">
${action.render_icon_and_label()}
</a>
&nbsp;
% endfor
</${b}-table-column>
% endif
<template #empty>
<section class="section">
<div class="content has-text-grey has-text-centered">
<p>
<b-icon
pack="fas"
icon="sad-tear"
size="is-large">
</b-icon>
</p>
<p>Nothing here.</p>
</div>
</section>
</template>
</${b}-table>

View file

@ -28,6 +28,7 @@ import sqlalchemy as sa
from wuttjamaican.db.model import Person from wuttjamaican.db.model import Person
from wuttaweb.views import MasterView from wuttaweb.views import MasterView
from wuttaweb.forms.schema import UserRefs
class PersonView(MasterView): class PersonView(MasterView):
@ -70,23 +71,25 @@ class PersonView(MasterView):
# last_name # last_name
g.set_link('last_name') g.set_link('last_name')
# TODO: master should handle this?
def configure_form(self, f): def configure_form(self, f):
""" """ """ """
super().configure_form(f) super().configure_form(f)
person = f.model_instance
# first_name # TODO: master should handle these? (nullable column)
f.set_required('first_name', False) f.set_required('first_name', False)
# middle_name
f.set_required('middle_name', False) f.set_required('middle_name', False)
# last_name
f.set_required('last_name', False) f.set_required('last_name', False)
# users # users
if 'users' in f: # nb. colanderalchemy wants to do some magic for the true
f.fields.remove('users') # 'users' relationship, so we use a different field name
f.remove('users')
if not (self.creating or self.editing):
f.append('_users')
f.set_readonly('_users')
f.set_node('_users', UserRefs(self.request))
f.set_default('_users', [u.uuid for u in person.users])
def autocomplete_query(self, term): def autocomplete_query(self, term):
""" """ """ """

View file

@ -28,7 +28,7 @@ from wuttjamaican.db.model import Role
from wuttaweb.views import MasterView from wuttaweb.views import MasterView
from wuttaweb.db import Session from wuttaweb.db import Session
from wuttaweb.forms import widgets from wuttaweb.forms import widgets
from wuttaweb.forms.schema import Permissions from wuttaweb.forms.schema import UserRefs, Permissions
class RoleView(MasterView): class RoleView(MasterView):
@ -115,6 +115,13 @@ class RoleView(MasterView):
# notes # notes
f.set_widget('notes', widgets.NotesWidget()) f.set_widget('notes', widgets.NotesWidget())
# users
if not (self.creating or self.editing):
f.append('users')
f.set_readonly('users')
f.set_node('users', UserRefs(self.request))
f.set_default('users', [u.uuid for u in role.users])
# permissions # permissions
f.append('permissions') f.append('permissions')
self.wutta_permissions = self.get_available_permissions() self.wutta_permissions = self.get_available_permissions()

View file

@ -10,6 +10,7 @@ from pyramid import testing
from wuttjamaican.conf import WuttaConfig from wuttjamaican.conf import WuttaConfig
from wuttaweb.forms import base, widgets from wuttaweb.forms import base, widgets
from wuttaweb import helpers from wuttaweb import helpers
from wuttaweb.grids import Grid
class TestForm(TestCase): class TestForm(TestCase):
@ -405,6 +406,29 @@ class TestForm(TestCase):
self.assertIn('<script type="text/x-template" id="wutta-form-template">', html) self.assertIn('<script type="text/x-template" id="wutta-form-template">', html)
self.assertNotIn('@submit', html) self.assertNotIn('@submit', html)
def test_add_grid_vue_data(self):
form = self.make_form()
# grid must have key
grid = Grid(self.request)
self.assertRaises(ValueError, form.add_grid_vue_data, grid)
# otherwise it works
grid = Grid(self.request, key='foo')
self.assertEqual(len(form.grid_vue_data), 0)
form.add_grid_vue_data(grid)
self.assertEqual(len(form.grid_vue_data), 1)
self.assertIn('foo', form.grid_vue_data)
self.assertEqual(form.grid_vue_data['foo'], [])
# calling again with same key will replace data
records = [{'foo': 1}, {'foo': 2}]
grid = Grid(self.request, key='foo', columns=['foo'], data=records)
form.add_grid_vue_data(grid)
self.assertEqual(len(form.grid_vue_data), 1)
self.assertIn('foo', form.grid_vue_data)
self.assertEqual(form.grid_vue_data['foo'], records)
def test_render_vue_finalize(self): def test_render_vue_finalize(self):
form = self.make_form() form = self.make_form()
html = form.render_vue_finalize() html = form.render_vue_finalize()

View file

@ -9,6 +9,7 @@ from sqlalchemy import orm
from wuttjamaican.conf import WuttaConfig from wuttjamaican.conf import WuttaConfig
from wuttaweb.forms import schema as mod from wuttaweb.forms import schema as mod
from wuttaweb.forms import widgets
from tests.util import DataTestCase from tests.util import DataTestCase
@ -200,6 +201,19 @@ class TestPersonRef(DataTestCase):
self.assertIsNot(sorted_query, query) self.assertIsNot(sorted_query, query)
class TestUserRefs(DataTestCase):
def setUp(self):
self.setup_db()
self.request = testing.DummyRequest(wutta_config=self.config)
def test_widget_maker(self):
model = self.app.model
typ = mod.UserRefs(self.request, session=self.session)
widget = typ.widget_maker()
self.assertIsInstance(widget, widgets.UserRefsWidget)
class TestRoleRefs(DataTestCase): class TestRoleRefs(DataTestCase):
def setUp(self): def setUp(self):

View file

@ -1,11 +1,13 @@
# -*- coding: utf-8; -*- # -*- coding: utf-8; -*-
from unittest.mock import patch
import colander import colander
import deform import deform
from pyramid import testing from pyramid import testing
from wuttaweb.forms import widgets as mod from wuttaweb.forms import widgets as mod
from wuttaweb.forms.schema import PersonRef, RoleRefs, Permissions from wuttaweb.forms.schema import PersonRef, RoleRefs, UserRefs, Permissions
from tests.util import WebTestCase from tests.util import WebTestCase
@ -36,7 +38,18 @@ class TestObjectRefWidget(WebTestCase):
widget = mod.ObjectRefWidget(self.request) widget = mod.ObjectRefWidget(self.request)
field = self.make_field(node) field = self.make_field(node)
html = widget.serialize(field, person.uuid, readonly=True) html = widget.serialize(field, person.uuid, readonly=True)
self.assertEqual(html.strip(), '<span>Betty Boop</span>') self.assertIn('Betty Boop', html)
self.assertNotIn('<a', html)
# with hyperlink
node = colander.SchemaNode(PersonRef(self.request, session=self.session))
node.model_instance = person
widget = mod.ObjectRefWidget(self.request, url=lambda p: '/foo')
field = self.make_field(node)
html = widget.serialize(field, person.uuid, readonly=True)
self.assertIn('Betty Boop', html)
self.assertIn('<a', html)
self.assertIn('href="/foo"', html)
class TestRoleRefsWidget(WebTestCase): class TestRoleRefsWidget(WebTestCase):
@ -48,6 +61,7 @@ class TestRoleRefsWidget(WebTestCase):
return deform.Field(node, **kwargs) return deform.Field(node, **kwargs)
def test_serialize(self): def test_serialize(self):
self.pyramid_config.add_route('roles.view', '/roles/{uuid}')
model = self.app.model model = self.app.model
auth = self.app.get_auth_handler() auth = self.app.get_auth_handler()
admin = auth.get_role_administrator(self.session) admin = auth.get_role_administrator(self.session)
@ -77,6 +91,49 @@ class TestRoleRefsWidget(WebTestCase):
self.assertIn(blokes.uuid, html) self.assertIn(blokes.uuid, html)
class TestUserRefsWidget(WebTestCase):
def make_field(self, node, **kwargs):
# TODO: not sure why default renderer is in use even though
# pyramid_deform was included in setup? but this works..
kwargs.setdefault('renderer', deform.Form.default_renderer)
return deform.Field(node, **kwargs)
def test_serialize(self):
model = self.app.model
# nb. we let the field construct the widget via our type
node = colander.SchemaNode(UserRefs(self.request, session=self.session))
field = self.make_field(node)
widget = field.widget
# readonly is required
self.assertRaises(NotImplementedError, widget.serialize, field, set())
self.assertRaises(NotImplementedError, widget.serialize, field, set(), readonly=False)
# empty
html = widget.serialize(field, set(), readonly=True)
self.assertIn('<b-table ', html)
# with data, no actions
user = model.User(username='barney')
self.session.add(user)
self.session.commit()
html = widget.serialize(field, {user.uuid}, readonly=True)
self.assertIn('<b-table ', html)
self.assertNotIn('Actions', html)
self.assertNotIn('View', html)
self.assertNotIn('Edit', html)
# with view/edit actions
with patch.object(self.request, 'is_root', new=True):
html = widget.serialize(field, {user.uuid}, readonly=True)
self.assertIn('<b-table ', html)
self.assertIn('Actions', html)
self.assertIn('View', html)
self.assertIn('Edit', html)
class TestPermissionsWidget(WebTestCase): class TestPermissionsWidget(WebTestCase):
def make_field(self, node, **kwargs): def make_field(self, node, **kwargs):

View file

@ -10,7 +10,8 @@ from pyramid import testing
from wuttjamaican.conf import WuttaConfig from wuttjamaican.conf import WuttaConfig
from wuttaweb.grids import base as mod from wuttaweb.grids import base as mod
from wuttaweb.forms import FieldList from wuttaweb.util import FieldList
from wuttaweb.forms import Form
from tests.util import WebTestCase from tests.util import WebTestCase
@ -186,6 +187,14 @@ class TestGrid(WebTestCase):
self.assertFalse(grid.is_linked('foo')) self.assertFalse(grid.is_linked('foo'))
self.assertTrue(grid.is_linked('bar')) self.assertTrue(grid.is_linked('bar'))
def test_add_action(self):
grid = self.make_grid()
self.assertEqual(len(grid.actions), 0)
grid.add_action('view')
self.assertEqual(len(grid.actions), 1)
self.assertIsInstance(grid.actions[0], mod.GridAction)
def test_get_pagesize_options(self): def test_get_pagesize_options(self):
grid = self.make_grid() grid = self.make_grid()
@ -855,6 +864,25 @@ class TestGrid(WebTestCase):
html = grid.render_vue_template() html = grid.render_vue_template()
self.assertIn('<script type="text/x-template" id="wutta-grid-template">', html) self.assertIn('<script type="text/x-template" id="wutta-grid-template">', html)
def test_render_table_element(self):
self.pyramid_config.include('pyramid_mako')
self.pyramid_config.add_subscriber('wuttaweb.subscribers.before_render',
'pyramid.events.BeforeRender')
grid = self.make_grid(key='foobar', columns=['foo', 'bar'])
# form not required
html = grid.render_table_element()
self.assertNotIn('<script ', html)
self.assertIn('<b-table ', html)
# form will register grid data
form = Form(self.request)
self.assertEqual(len(form.grid_vue_data), 0)
html = grid.render_table_element(form)
self.assertEqual(len(form.grid_vue_data), 1)
self.assertIn('foobar', form.grid_vue_data)
def test_render_vue_finalize(self): def test_render_vue_finalize(self):
grid = self.make_grid() grid = self.make_grid()
html = grid.render_vue_finalize() html = grid.render_vue_finalize()

View file

@ -35,12 +35,27 @@ class TestPersonView(WebTestCase):
model = self.app.model model = self.app.model
view = self.make_view() view = self.make_view()
form = view.make_form(model_class=model.Person) form = view.make_form(model_class=model.Person)
# required fields
with patch.object(view, 'creating', new=True):
form.set_fields(form.get_model_fields()) form.set_fields(form.get_model_fields())
self.assertEqual(form.required_fields, {}) self.assertEqual(form.required_fields, {})
view.configure_form(form) view.configure_form(form)
self.assertTrue(form.required_fields) self.assertTrue(form.required_fields)
self.assertFalse(form.required_fields['middle_name']) self.assertFalse(form.required_fields['middle_name'])
person = model.Person(full_name="Barney Rubble")
user = model.User(username='barney', person=person)
self.session.add(user)
self.session.commit()
# users field
with patch.object(view, 'viewing', new=True):
form = view.make_form(model_instance=person)
self.assertEqual(form.defaults, {})
view.configure_form(form)
self.assertIn('_users', form.defaults)
def test_autocomplete_query(self): def test_autocomplete_query(self):
model = self.app.model model = self.app.model