590 lines
22 KiB
Python
590 lines
22 KiB
Python
# -*- coding: utf-8; -*-
|
|
|
|
from unittest import TestCase
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import colander
|
|
import deform
|
|
from pyramid import testing
|
|
|
|
from wuttjamaican.conf import WuttaConfig
|
|
from wuttaweb.forms import base, widgets
|
|
from wuttaweb import helpers
|
|
from wuttaweb.grids import Grid
|
|
|
|
|
|
class TestForm(TestCase):
|
|
|
|
def setUp(self):
|
|
self.config = WuttaConfig(defaults={
|
|
'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)
|
|
|
|
self.pyramid_config = testing.setUp(request=self.request, settings={
|
|
'mako.directories': ['wuttaweb:templates'],
|
|
'pyramid_deform.template_search_path': 'wuttaweb:templates/deform',
|
|
})
|
|
|
|
def tearDown(self):
|
|
testing.tearDown()
|
|
|
|
def make_form(self, **kwargs):
|
|
return base.Form(self.request, **kwargs)
|
|
|
|
def make_schema(self):
|
|
schema = colander.Schema(children=[
|
|
colander.SchemaNode(colander.String(),
|
|
name='foo'),
|
|
colander.SchemaNode(colander.String(),
|
|
name='bar'),
|
|
])
|
|
return schema
|
|
|
|
def test_init_with_none(self):
|
|
form = self.make_form()
|
|
self.assertEqual(form.fields, [])
|
|
|
|
def test_init_with_fields(self):
|
|
form = self.make_form(fields=['foo', 'bar'])
|
|
self.assertEqual(form.fields, ['foo', 'bar'])
|
|
|
|
def test_init_with_schema(self):
|
|
schema = self.make_schema()
|
|
form = self.make_form(schema=schema)
|
|
self.assertEqual(form.fields, ['foo', 'bar'])
|
|
|
|
def test_vue_tagname(self):
|
|
form = self.make_form()
|
|
self.assertEqual(form.vue_tagname, 'wutta-form')
|
|
|
|
def test_vue_component(self):
|
|
form = self.make_form()
|
|
self.assertEqual(form.vue_component, 'WuttaForm')
|
|
|
|
def test_contains(self):
|
|
form = self.make_form(fields=['foo', 'bar'])
|
|
self.assertIn('foo', form)
|
|
self.assertNotIn('baz', form)
|
|
|
|
def test_iter(self):
|
|
form = self.make_form(fields=['foo', 'bar'])
|
|
|
|
fields = list(iter(form))
|
|
self.assertEqual(fields, ['foo', 'bar'])
|
|
|
|
fields = []
|
|
for field in form:
|
|
fields.append(field)
|
|
self.assertEqual(fields, ['foo', 'bar'])
|
|
|
|
def test_set_fields(self):
|
|
form = self.make_form(fields=['foo', 'bar'])
|
|
self.assertEqual(form.fields, ['foo', 'bar'])
|
|
form.set_fields(['baz'])
|
|
self.assertEqual(form.fields, ['baz'])
|
|
|
|
def test_append(self):
|
|
form = self.make_form(fields=['one', 'two'])
|
|
self.assertEqual(form.fields, ['one', 'two'])
|
|
form.append('one', 'two', 'three')
|
|
self.assertEqual(form.fields, ['one', 'two', 'three'])
|
|
|
|
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_set_default(self):
|
|
form = self.make_form(fields=['foo', 'bar'])
|
|
self.assertEqual(form.defaults, {})
|
|
|
|
# basic
|
|
form.set_default('foo', 42)
|
|
self.assertEqual(form.defaults['foo'], 42)
|
|
|
|
def test_get_schema(self):
|
|
model = self.app.model
|
|
form = self.make_form()
|
|
self.assertIsNone(form.schema)
|
|
|
|
# provided schema is returned
|
|
schema = self.make_schema()
|
|
form = self.make_form(schema=schema)
|
|
self.assertIs(form.schema, schema)
|
|
self.assertIs(form.get_schema(), schema)
|
|
|
|
# schema is auto-generated if fields provided
|
|
form = self.make_form(fields=['foo', 'bar'])
|
|
schema = form.get_schema()
|
|
self.assertEqual(len(schema.children), 2)
|
|
self.assertEqual(schema['foo'].name, 'foo')
|
|
|
|
# but auto-generating without fields is not supported
|
|
form = self.make_form()
|
|
self.assertIsNone(form.schema)
|
|
self.assertRaises(NotImplementedError, form.get_schema)
|
|
|
|
# schema is auto-generated if model_class provided
|
|
form = self.make_form(model_class=model.Setting)
|
|
schema = form.get_schema()
|
|
self.assertEqual(len(schema.children), 2)
|
|
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'])
|
|
self.assertIsNone(form.schema)
|
|
# nb. force method to get new fields
|
|
del form.fields
|
|
schema = form.get_schema()
|
|
self.assertEqual(len(schema.children), 2)
|
|
self.assertIn('name', schema)
|
|
self.assertIn('value', schema)
|
|
|
|
# ColanderAlchemy schema still has *all* requested fields
|
|
form = self.make_form(model_instance=model.Setting(name='uhoh'),
|
|
fields=['name', 'value', 'foo', 'bar'])
|
|
self.assertEqual(form.fields, ['name', 'value', 'foo', 'bar'])
|
|
self.assertIsNone(form.schema)
|
|
schema = form.get_schema()
|
|
self.assertEqual(len(schema.children), 4)
|
|
self.assertIn('name', schema)
|
|
self.assertIn('value', schema)
|
|
self.assertIn('foo', schema)
|
|
self.assertIn('bar', schema)
|
|
|
|
# schema nodes are required by default
|
|
form = self.make_form(fields=['foo', 'bar'])
|
|
schema = form.get_schema()
|
|
self.assertIs(schema['foo'].missing, colander.required)
|
|
self.assertIs(schema['bar'].missing, colander.required)
|
|
|
|
# but fields can be marked *not* required
|
|
form = self.make_form(fields=['foo', 'bar'])
|
|
form.set_required('bar', False)
|
|
schema = form.get_schema()
|
|
self.assertIs(schema['foo'].missing, colander.required)
|
|
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)
|
|
|
|
# default value overrides are honored
|
|
form = self.make_form(model_class=model.Setting)
|
|
form.set_default('name', 'foo')
|
|
schema = form.get_schema()
|
|
self.assertEqual(schema['name'].default, 'foo')
|
|
|
|
def test_get_deform(self):
|
|
model = self.app.model
|
|
schema = self.make_schema()
|
|
|
|
# basic
|
|
form = self.make_form(schema=schema)
|
|
self.assertFalse(hasattr(form, 'deform_form'))
|
|
dform = form.get_deform()
|
|
self.assertIsInstance(dform, deform.Form)
|
|
self.assertIs(form.deform_form, dform)
|
|
|
|
# with model instance as dict
|
|
myobj = {'foo': 'one', 'bar': 'two'}
|
|
form = self.make_form(schema=schema, model_instance=myobj)
|
|
dform = form.get_deform()
|
|
self.assertEqual(dform.cstruct, myobj)
|
|
|
|
# with sqlalchemy model instance
|
|
myobj = model.Setting(name='foo', value='bar')
|
|
form = self.make_form(model_instance=myobj)
|
|
dform = form.get_deform()
|
|
self.assertEqual(dform.cstruct, {'name': 'foo', 'value': 'bar'})
|
|
|
|
# sqlalchemy instance with null value
|
|
myobj = model.Setting(name='foo', value=None)
|
|
form = self.make_form(model_instance=myobj)
|
|
dform = form.get_deform()
|
|
self.assertEqual(dform.cstruct, {'name': 'foo', 'value': colander.null})
|
|
|
|
def test_get_cancel_url(self):
|
|
|
|
# is referrer by default
|
|
form = self.make_form()
|
|
self.request.get_referrer = MagicMock(return_value='/cancel-default')
|
|
self.assertEqual(form.get_cancel_url(), '/cancel-default')
|
|
del self.request.get_referrer
|
|
|
|
# or can be static URL
|
|
form = self.make_form(cancel_url='/cancel-static')
|
|
self.assertEqual(form.get_cancel_url(), '/cancel-static')
|
|
|
|
# or can be fallback URL (nb. 'NOPE' indicates no referrer)
|
|
form = self.make_form(cancel_url_fallback='/cancel-fallback')
|
|
self.request.get_referrer = MagicMock(return_value='NOPE')
|
|
self.assertEqual(form.get_cancel_url(), '/cancel-fallback')
|
|
del self.request.get_referrer
|
|
|
|
# or can be referrer fallback, i.e. home page
|
|
form = self.make_form()
|
|
def get_referrer(default=None):
|
|
if default == 'NOPE':
|
|
return 'NOPE'
|
|
return '/home-page'
|
|
self.request.get_referrer = get_referrer
|
|
self.assertEqual(form.get_cancel_url(), '/home-page')
|
|
del self.request.get_referrer
|
|
|
|
def test_get_label(self):
|
|
form = self.make_form(fields=['foo', 'bar'])
|
|
self.assertEqual(form.get_label('foo'), "Foo")
|
|
form.set_label('foo', "Baz")
|
|
self.assertEqual(form.get_label('foo'), "Baz")
|
|
|
|
def test_set_label(self):
|
|
form = self.make_form(fields=['foo', 'bar'])
|
|
self.assertEqual(form.get_label('foo'), "Foo")
|
|
form.set_label('foo', "Baz")
|
|
self.assertEqual(form.get_label('foo'), "Baz")
|
|
|
|
# schema should be updated when setting label
|
|
schema = self.make_schema()
|
|
form = self.make_form(schema=schema)
|
|
form.set_label('foo', "Woohoo")
|
|
self.assertEqual(form.get_label('foo'), "Woohoo")
|
|
self.assertEqual(schema['foo'].title, "Woohoo")
|
|
|
|
def test_readonly_fields(self):
|
|
form = self.make_form(fields=['foo', 'bar'])
|
|
self.assertEqual(form.readonly_fields, set())
|
|
self.assertFalse(form.is_readonly('foo'))
|
|
|
|
form.set_readonly('foo')
|
|
self.assertEqual(form.readonly_fields, {'foo'})
|
|
self.assertTrue(form.is_readonly('foo'))
|
|
self.assertFalse(form.is_readonly('bar'))
|
|
|
|
form.set_readonly('bar')
|
|
self.assertEqual(form.readonly_fields, {'foo', 'bar'})
|
|
self.assertTrue(form.is_readonly('foo'))
|
|
self.assertTrue(form.is_readonly('bar'))
|
|
|
|
form.set_readonly('foo', False)
|
|
self.assertEqual(form.readonly_fields, {'bar'})
|
|
self.assertFalse(form.is_readonly('foo'))
|
|
self.assertTrue(form.is_readonly('bar'))
|
|
|
|
def test_required_fields(self):
|
|
form = self.make_form(fields=['foo', 'bar'])
|
|
self.assertEqual(form.required_fields, {})
|
|
self.assertIsNone(form.is_required('foo'))
|
|
|
|
form.set_required('foo')
|
|
self.assertEqual(form.required_fields, {'foo': True})
|
|
self.assertTrue(form.is_required('foo'))
|
|
self.assertIsNone(form.is_required('bar'))
|
|
|
|
form.set_required('bar')
|
|
self.assertEqual(form.required_fields, {'foo': True, 'bar': True})
|
|
self.assertTrue(form.is_required('foo'))
|
|
self.assertTrue(form.is_required('bar'))
|
|
|
|
form.set_required('foo', False)
|
|
self.assertEqual(form.required_fields, {'foo': False, 'bar': True})
|
|
self.assertFalse(form.is_required('foo'))
|
|
self.assertTrue(form.is_required('bar'))
|
|
|
|
def test_render_vue_tag(self):
|
|
schema = self.make_schema()
|
|
form = self.make_form(schema=schema)
|
|
html = form.render_vue_tag()
|
|
self.assertEqual(html, '<wutta-form></wutta-form>')
|
|
|
|
def test_render_vue_template(self):
|
|
self.pyramid_config.include('pyramid_mako')
|
|
self.pyramid_config.add_subscriber('wuttaweb.subscribers.before_render',
|
|
'pyramid.events.BeforeRender')
|
|
|
|
# form button is disabled on @submit by default
|
|
schema = self.make_schema()
|
|
form = self.make_form(schema=schema, cancel_url='/')
|
|
html = form.render_vue_template()
|
|
self.assertIn('<script type="text/x-template" id="wutta-form-template">', html)
|
|
self.assertIn('@submit', html)
|
|
|
|
# but not if form is configured otherwise
|
|
form = self.make_form(schema=schema, auto_disable_submit=False, cancel_url='/')
|
|
html = form.render_vue_template()
|
|
self.assertIn('<script type="text/x-template" id="wutta-form-template">', html)
|
|
self.assertNotIn('@submit', html)
|
|
|
|
def test_add_grid_vue_context(self):
|
|
form = self.make_form()
|
|
|
|
# grid must have key
|
|
grid = Grid(self.request)
|
|
self.assertRaises(ValueError, form.add_grid_vue_context, grid)
|
|
|
|
# otherwise it works
|
|
grid = Grid(self.request, key='foo')
|
|
self.assertEqual(len(form.grid_vue_context), 0)
|
|
form.add_grid_vue_context(grid)
|
|
self.assertEqual(len(form.grid_vue_context), 1)
|
|
self.assertIn('foo', form.grid_vue_context)
|
|
self.assertEqual(form.grid_vue_context['foo'], {
|
|
'data': [],
|
|
'row_classes': {},
|
|
})
|
|
|
|
# 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_context(grid)
|
|
self.assertEqual(len(form.grid_vue_context), 1)
|
|
self.assertIn('foo', form.grid_vue_context)
|
|
self.assertEqual(form.grid_vue_context['foo'], {
|
|
'data': records,
|
|
'row_classes': {},
|
|
})
|
|
|
|
def test_render_vue_finalize(self):
|
|
form = self.make_form()
|
|
html = form.render_vue_finalize()
|
|
self.assertIn('<script>', html)
|
|
self.assertIn("Vue.component('wutta-form', WuttaForm)", html)
|
|
|
|
def test_render_vue_field(self):
|
|
self.pyramid_config.include('pyramid_deform')
|
|
schema = self.make_schema()
|
|
form = self.make_form(schema=schema)
|
|
dform = form.get_deform()
|
|
|
|
# typical
|
|
html = form.render_vue_field('foo')
|
|
self.assertIn('<b-field :horizontal="true" label="Foo">', html)
|
|
self.assertIn('<b-input name="foo"', html)
|
|
# nb. no error message
|
|
self.assertNotIn('message', html)
|
|
|
|
# readonly
|
|
html = form.render_vue_field('foo', readonly=True)
|
|
self.assertIn('<b-field :horizontal="true" label="Foo">', html)
|
|
self.assertNotIn('<b-input name="foo"', html)
|
|
# nb. no error message
|
|
self.assertNotIn('message', html)
|
|
|
|
# with error message
|
|
with patch.object(form, 'get_field_errors', return_value=['something is wrong']):
|
|
html = form.render_vue_field('foo')
|
|
self.assertIn('something is wrong', html)
|
|
|
|
# add another field, but not to deform, so it should still
|
|
# display but with no widget
|
|
form.fields.append('zanzibar')
|
|
html = form.render_vue_field('zanzibar')
|
|
self.assertIn('<b-field :horizontal="true" label="Zanzibar">', html)
|
|
self.assertNotIn('<b-input', html)
|
|
# nb. no error message
|
|
self.assertNotIn('message', html)
|
|
|
|
# try that once more but with a model record instance
|
|
with patch.object(form, 'model_instance', new={'zanzibar': 'omgwtfbbq'}):
|
|
html = form.render_vue_field('zanzibar')
|
|
self.assertIn('<b-field', html)
|
|
self.assertIn('label="Zanzibar"', html)
|
|
self.assertNotIn('<b-input', html)
|
|
self.assertIn('>omgwtfbbq<', html)
|
|
# nb. no error message
|
|
self.assertNotIn('message', html)
|
|
|
|
def test_get_vue_field_value(self):
|
|
schema = self.make_schema()
|
|
form = self.make_form(schema=schema)
|
|
|
|
# TODO: yikes what a hack (?)
|
|
dform = form.get_deform()
|
|
dform.set_appstruct({'foo': 'one', 'bar': 'two'})
|
|
|
|
# null for missing field
|
|
value = form.get_vue_field_value('doesnotexist')
|
|
self.assertIsNone(value)
|
|
|
|
# normal value is returned
|
|
value = form.get_vue_field_value('foo')
|
|
self.assertEqual(value, 'one')
|
|
|
|
# but not if we remove field from deform
|
|
# TODO: what is the use case here again?
|
|
dform.children.remove(dform['foo'])
|
|
value = form.get_vue_field_value('foo')
|
|
self.assertIsNone(value)
|
|
|
|
def test_get_vue_model_data(self):
|
|
schema = self.make_schema()
|
|
form = self.make_form(schema=schema)
|
|
|
|
# 2 fields by default (foo, bar)
|
|
data = form.get_vue_model_data()
|
|
self.assertEqual(len(data), 2)
|
|
|
|
# still just 2 fields even if we request more
|
|
form.set_fields(['foo', 'bar', 'baz'])
|
|
data = form.get_vue_model_data()
|
|
self.assertEqual(len(data), 2)
|
|
|
|
# confirm bool values make it thru as-is
|
|
schema.add(colander.SchemaNode(colander.Bool(), name='baz'))
|
|
form = self.make_form(schema=schema, model_instance={
|
|
'foo': 'one',
|
|
'bar': 'two',
|
|
'baz': True,
|
|
})
|
|
data = form.get_vue_model_data()
|
|
self.assertEqual(list(data.values()), ['one', 'two', True])
|
|
|
|
def test_get_field_errors(self):
|
|
schema = self.make_schema()
|
|
|
|
# simple 'Required' validation failure
|
|
form = self.make_form(schema=schema)
|
|
self.request.method = 'POST'
|
|
self.request.POST = {'foo': 'one'}
|
|
self.assertFalse(form.validate())
|
|
errors = form.get_field_errors('bar')
|
|
self.assertEqual(errors, ['Required'])
|
|
|
|
# no errors
|
|
form = self.make_form(schema=schema)
|
|
self.request.POST = {'foo': 'one', 'bar': 'two'}
|
|
self.assertTrue(form.validate())
|
|
errors = form.get_field_errors('bar')
|
|
self.assertEqual(errors, [])
|
|
|
|
def test_validate(self):
|
|
schema = self.make_schema()
|
|
form = self.make_form(schema=schema)
|
|
self.assertFalse(hasattr(form, 'validated'))
|
|
|
|
# will not validate unless request is POST
|
|
self.request.POST = {'foo': 'blarg', 'bar': 'baz'}
|
|
self.request.method = 'GET'
|
|
self.assertFalse(form.validate())
|
|
self.request.method = 'POST'
|
|
data = form.validate()
|
|
self.assertEqual(data, {'foo': 'blarg', 'bar': 'baz'})
|
|
|
|
# validating a second time updates form.validated
|
|
self.request.POST = {'foo': 'BLARG', 'bar': 'BAZ'}
|
|
data = form.validate()
|
|
self.assertEqual(data, {'foo': 'BLARG', 'bar': 'BAZ'})
|
|
self.assertIs(form.validated, data)
|
|
|
|
# bad data does not validate
|
|
self.request.POST = {'foo': 42, 'bar': None}
|
|
self.assertFalse(form.validate())
|
|
dform = form.get_deform()
|
|
self.assertEqual(len(dform.error.children), 2)
|
|
self.assertEqual(dform['foo'].errormsg, "Pstruct is not a string")
|
|
|
|
# when a form has readonly fields, validating it will *remove*
|
|
# those fields from deform/schema as well as final data dict
|
|
schema = self.make_schema()
|
|
form = self.make_form(schema=schema)
|
|
form.set_readonly('foo')
|
|
self.request.POST = {'foo': 'one', 'bar': 'two'}
|
|
data = form.validate()
|
|
self.assertEqual(data, {'bar': 'two'})
|
|
dform = form.get_deform()
|
|
self.assertNotIn('foo', schema)
|
|
self.assertNotIn('foo', dform)
|
|
self.assertIn('bar', schema)
|
|
self.assertIn('bar', dform)
|