We’ve come quite far with the basic investigation of using FormEncode, lxml.html, and ZPT to handle our form needs. Our “form system” is under 80 lines.
Let’s take this chance to refactor into a more regular style of application:
writing code, then reloading in a browser to see if it worked. Running tests is the better pattern for the code-check-fix cycle. Also, we need to make sure we are design a forms system that can test the various flows of control inside a view.
Actual models with persisten ZODB content. Using interfaces and classes, we can have edit forms that better match the form patterns we’ll actually be using.
We will also change the templates to load jQuery (used in later installments). We will also try to leverage the FormEncode concept of application “state” which can be passed into the validators.
Currently, if we run the tests, we see that both are broken:
(t3)bash-3.2$ python setup.py test [snip] ====================================================================== FAIL: test_my_view (t3.tests.ViewIntegrationTests) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/paul/venvs/t3/t3/t3/tests.py", line 68, in test_my_view self.failUnless('Welcome to' in body) AssertionError ====================================================================== FAIL: test_my_view (t3.tests.ViewTests) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/paul/venvs/t3/t3/t3/tests.py", line 31, in test_my_view renderer.assert_(project='t3') File "testing.py", line 215, in assert_ 'A value for key "%s" was not passed to the renderer' % k) AssertionError: A value for key "project" was not passed to the renderer ---------------------------------------------------------------------- Ran 2 tests in 1.091s FAILED (failures=2)
This is simple to fix: we changed the templates to no longer require the project name to be passed in, and instead hard-coded them in the template. Three small changes in t3/tests.py.
# Line 31 self.assert_(hasattr(renderer, 'layout')) # Line 68 self.failUnless('KARL3 Forms' in body)
Now the tests run again and we can start refactoring.
Let’s start by doing some refactoring that has little to do with building a form system, and more to do with how we’ll use forms. Let’s convert the application to add/view/edit/delete Person content.
Make a t3/interfaces.py:
1 2 3 4 5 6 7 8 9 10 | from zope.interface import Interface
from zope.interface import Attribute
from repoze.folder.interfaces import IFolder
class ISite(IFolder):
pass
class IPerson(Interface):
name = Attribute(u'The name of the person')
age = Attribute(u'An integer representing the age')
|
Change our t3/models.py to use different jargon, assert interfaces, use a constructor, etc.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | from zope.interface import implements
from repoze.folder import Folder
from t3.interfaces import ISite
from t3.interfaces import IPerson
class Site(Folder):
implements(ISite)
class Person(Folder):
implements(IPerson)
def __init__(self, name, age, country=None):
Folder.__init__(self)
self.name = unicode(name)
self.age = age
if country is not None:
self.country = unicode(country)
def appmaker(zodb_root):
if not 'app_root' in zodb_root:
app_root = Site()
zodb_root['app_root'] = app_root
import transaction
transaction.commit()
return zodb_root['app_root']
|
The ZCML needs to reflect the new order, as well as point at appropriate views:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | <configure xmlns="http://namespaces.repoze.org/bfg">
<!-- this must be included for the view declarations to work -->
<include package="repoze.bfg.includes" />
<view
for=".models.Site"
view=".views.list_people_view"
/>
<view
for=".models.Site"
view=".views.add_person_view"
name="add_person.html"
/>
<view
for=".models.Person"
view=".views.show_person_view"
/>
<view
for=".models.Person"
view=".views.edit_person_view"
name="edit.html"
/>
<view
for=".models.Site"
view=".views.static_view"
name="static"
/>
</configure>
|
Finally, update the tests in t3/tests.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 | import unittest
from zope.testing.cleanup import cleanUp
from repoze.bfg import testing
class SiteModelTests(unittest.TestCase):
def setUp(self):
""" cleanUp() is required to clear out the application registry
between tests (done in setUp for good measure too)
"""
cleanUp()
def tearDown(self):
""" cleanUp() is required to clear out the application registry
between tests
"""
cleanUp()
def _getTargetClass(self):
from t3.models import Site
return Site
def _makeOne(self):
tc = self._getTargetClass()
return tc()
def test_class_conforms_to_ISite(self):
from zope.interface.verify import verifyClass
from t3.models import ISite
verifyClass(ISite, self._getTargetClass())
def test_instance_conforms_to_IFeed(self):
from zope.interface.verify import verifyObject
from t3.models import ISite
verifyObject(ISite, self._makeOne())
class PersonModelTests(unittest.TestCase):
def setUp(self):
""" cleanUp() is required to clear out the application registry
between tests (done in setUp for good measure too)
"""
cleanUp()
def tearDown(self):
""" cleanUp() is required to clear out the application registry
between tests
"""
cleanUp()
def _getTargetClass(self):
from t3.models import Person
return Person
def _makeOne(self, name=u'Dummy', age=30, country=u'FR'):
tc = self._getTargetClass()
return tc(name, age, country)
def test_class_conforms_to_IPerson(self):
from zope.interface.verify import verifyClass
from t3.models import IPerson
verifyClass(IPerson, self._getTargetClass())
def test_instance_conforms_to_IFeed(self):
from zope.interface.verify import verifyObject
from t3.models import IPerson
verifyObject(IPerson, self._makeOne())
class ViewTests(unittest.TestCase):
""" These tests are unit tests for the view. They test the
functionality of *only* the view. They register and use dummy
implementations of repoze.bfg functionality to allow you to avoid
testing 'too much'"""
def setUp(self):
""" cleanUp() is required to clear out the application registry
between tests (done in setUp for good measure too)
"""
cleanUp()
def tearDown(self):
""" cleanUp() is required to clear out the application registry
between tests
"""
cleanUp()
def test_list_people(self):
from t3.views import list_people_view
context = testing.DummyModel()
request = testing.DummyRequest()
renderer = testing.registerDummyRenderer('templates/list_people.pt')
response = list_people_view(context, request)
self.assert_(hasattr(renderer, 'layout'))
def test_add_person(self):
from t3.views import add_person_view
context = testing.DummyModel()
request = testing.DummyRequest()
renderer = testing.registerDummyRenderer('templates/add_person.pt')
response = add_person_view(context, request)
self.assert_(hasattr(renderer, 'layout'))
def test_show_person(self):
from t3.views import show_person_view
context = DummyPerson()
request = testing.DummyRequest()
renderer = testing.registerDummyRenderer('templates/show_person.pt')
response = show_person_view(context, request)
self.assert_(hasattr(renderer, 'layout'))
def test_edit_person(self):
from t3.views import edit_person_view
context = DummyPerson()
request = testing.DummyRequest()
renderer = testing.registerDummyRenderer('templates/edit_person.pt')
response = edit_person_view(context, request)
self.assert_(hasattr(renderer, 'layout'))
class ViewIntegrationTests(unittest.TestCase):
""" These tests are integration tests for the view. These test
the functionality the view *and* its integration with the rest of
the repoze.bfg framework. They cause the entire environment to be
set up and torn down as if your application was running 'for
real'. This is a heavy-hammer way of making sure that your tests
have enough context to run properly, and it tests your view's
integration with the rest of BFG. You should not use this style
of test to perform 'true' unit testing as tests will run faster
and will be easier to write if you use the testing facilities
provided by bfg and only the registrations you need, as in the
above ViewTests.
"""
def setUp(self):
""" This sets up the application registry with the
registrations your application declares in its configure.zcml
(including dependent registrations for repoze.bfg itself).
"""
cleanUp()
import t3
import zope.configuration.xmlconfig
zope.configuration.xmlconfig.file('configure.zcml',
package=t3)
def tearDown(self):
""" Clear out the application registry """
cleanUp()
def test_list_people_view(self):
from t3.views import list_people_view
context = testing.DummyModel()
request = testing.DummyRequest()
result = list_people_view(context, request)
self.assertEqual(result.status, '200 OK')
body = result.app_iter[0]
self.failUnless('KARL3 Forms' in body)
self.assertEqual(len(result.headerlist), 2)
self.assertEqual(result.headerlist[0],
('Content-Type', 'text/html; charset=UTF-8'))
self.assertEqual(result.headerlist[1], ('Content-Length',
str(len(body))))
def test_add_person_view(self):
from t3.views import add_person_view
context = testing.DummyModel()
request = testing.DummyRequest()
result = add_person_view(context, request)
self.assertEqual(result.status, '200 OK')
body = result.app_iter[0]
self.failUnless('KARL3 Forms' in body)
self.assertEqual(len(result.headerlist), 2)
self.assertEqual(result.headerlist[0],
('Content-Type', 'text/html; charset=UTF-8'))
self.assertEqual(result.headerlist[1], ('Content-Length',
str(len(body))))
def test_show_person_view(self):
from t3.views import show_person_view
context = DummyPerson()
request = testing.DummyRequest()
result = show_person_view(context, request)
self.assertEqual(result.status, '200 OK')
body = result.app_iter[0]
self.failUnless('KARL3 Forms' in body)
self.assertEqual(len(result.headerlist), 2)
self.assertEqual(result.headerlist[0],
('Content-Type', 'text/html; charset=UTF-8'))
self.assertEqual(result.headerlist[1], ('Content-Length',
str(len(body))))
def test_edit_person_view(self):
from t3.views import edit_person_view
context = DummyPerson()
request = testing.DummyRequest()
result = edit_person_view(context, request)
self.assertEqual(result.status, '200 OK')
body = result.app_iter[0]
self.failUnless('KARL3 Forms' in body)
self.assertEqual(len(result.headerlist), 2)
self.assertEqual(result.headerlist[0],
('Content-Type', 'text/html; charset=UTF-8'))
self.assertEqual(result.headerlist[1], ('Content-Length',
str(len(body))))
class DummyPerson(testing.DummyModel):
name = u'Dummy Name'
age = 23
country = u'FR'
|
We made some changes to both the ZCML, so we need to change the views and view tests.
Changes in t3/views.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | from repoze.bfg.chameleon_zpt import render_template_to_response
from repoze.bfg.chameleon_zpt import get_template
from repoze.bfg.chameleon_zpt import render_template
from repoze.bfg.view import static
from webob.exc import HTTPFound
from repoze.bfg.url import model_url
from baseform import BaseForm
from baseform import name
from baseform import age
from baseform import country
from baseform import vocabularies
from t3.models import Person
from formencode import Invalid
from random import randrange
static_view = static('templates/static')
# Define a form schema, make an instance
class MyInfo(BaseForm):
name = name
age = age
country = country
myinfo = MyInfo
def list_people_view(context, request):
layout = get_template('templates/layout.pt')
return render_template_to_response(
'templates/list_people.pt',
context = context,
request = request,
page_title='List People',
layout=layout,
)
def add_person_view(context, request):
layout = get_template('templates/layout.pt')
formfields = get_template('templates/formfields.pt')
form = myinfo()
if form.is_submitted(request):
fieldvalues = form.combine_dicts(form.defaults, request.POST)
# Now validate
try:
fv = fieldvalues = form.to_python(fieldvalues)
fielderrors = {}
# Go ahead and do the work to add content, etc.
person = Person(fv['name'], fv['age'], fv['country'])
__name__ = str(randrange(0, 99999))
context[__name__] = person
return HTTPFound(location=model_url(person, request))
except Invalid, e:
fielderrors = e.error_dict
else:
fieldvalues = form.defaults
fielderrors = {}
# Render the form and shove some default values in
form_html = render_template(
'templates/myform.pt',
context = context,
request=request,
formfields=formfields,
fielderrors=fielderrors,
vocabularies=vocabularies,
)
rendered_form = form.merge(form_html, fieldvalues)
return render_template_to_response(
'templates/add_person.pt',
context=context,
request = request,
page_title='Add Person',
layout=layout,
form_html=rendered_form,
elapsed=form.elapsed,
)
def show_person_view(context, request):
layout = get_template('templates/layout.pt')
return render_template_to_response(
'templates/show_person.pt',
context = context,
request = request,
page_title='Show Person',
layout=layout,
)
def edit_person_view(context, request):
layout = get_template('templates/layout.pt')
formfields = get_template('templates/formfields.pt')
form = myinfo()
if form.is_submitted(request):
fieldvalues = form.combine_dicts(context.__dict__, request.POST)
# Now validate
try:
fieldvalues = form.to_python(fieldvalues)
fielderrors = {}
# Go ahead and do the work to update content, etc.
context.name = fieldvalues['name']
context.age = fieldvalues['age']
context.country = fieldvalues['country']
return HTTPFound(location=model_url(context, request))
except Invalid, e:
fielderrors = e.error_dict
else:
fieldvalues = context.__dict__
fielderrors = {}
# Render the form and shove some default values in
form_html = render_template(
'templates/myform.pt',
request=request,
formfields=formfields,
fielderrors=fielderrors,
vocabularies=vocabularies,
)
rendered_form = form.merge(form_html, fieldvalues)
return render_template_to_response(
'templates/edit_person.pt',
context = context,
request = request,
page_title='Edit %s' % context.name,
layout=layout,
form_html=rendered_form,
elapsed=form.elapsed,
)
|
Also in t3/baseform.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | from repoze.bfg.chameleon_zpt import render_template_to_response
from repoze.bfg.chameleon_zpt import get_template
from repoze.bfg.chameleon_zpt import render_template
from repoze.bfg.view import static
from webob.exc import HTTPFound
from repoze.bfg.url import model_url
from baseform import BaseForm
from baseform import name
from baseform import age
from baseform import country
from baseform import vocabularies
from t3.models import Person
from formencode import Invalid
from random import randrange
static_view = static('templates/static')
# Define a form schema, make an instance
class MyInfo(BaseForm):
name = name
age = age
country = country
myinfo = MyInfo
def list_people_view(context, request):
layout = get_template('templates/layout.pt')
return render_template_to_response(
'templates/list_people.pt',
context = context,
request = request,
page_title='List People',
layout=layout,
)
def add_person_view(context, request):
layout = get_template('templates/layout.pt')
formfields = get_template('templates/formfields.pt')
form = myinfo()
if form.is_submitted(request):
fieldvalues = form.combine_dicts(form.defaults, request.POST)
# Now validate
try:
fv = fieldvalues = form.to_python(fieldvalues)
fielderrors = {}
# Go ahead and do the work to add content, etc.
person = Person(fv['name'], fv['age'], fv['country'])
__name__ = str(randrange(0, 99999))
context[__name__] = person
return HTTPFound(location=model_url(person, request))
except Invalid, e:
fielderrors = e.error_dict
else:
fieldvalues = form.defaults
fielderrors = {}
# Render the form and shove some default values in
form_html = render_template(
'templates/myform.pt',
context = context,
request=request,
formfields=formfields,
fielderrors=fielderrors,
vocabularies=vocabularies,
)
rendered_form = form.merge(form_html, fieldvalues)
return render_template_to_response(
'templates/add_person.pt',
context=context,
request = request,
page_title='Add Person',
layout=layout,
form_html=rendered_form,
elapsed=form.elapsed,
)
def show_person_view(context, request):
layout = get_template('templates/layout.pt')
return render_template_to_response(
'templates/show_person.pt',
context = context,
request = request,
page_title='Show Person',
layout=layout,
)
def edit_person_view(context, request):
layout = get_template('templates/layout.pt')
formfields = get_template('templates/formfields.pt')
form = myinfo()
if form.is_submitted(request):
fieldvalues = form.combine_dicts(context.__dict__, request.POST)
# Now validate
try:
fieldvalues = form.to_python(fieldvalues)
fielderrors = {}
# Go ahead and do the work to update content, etc.
context.name = fieldvalues['name']
context.age = fieldvalues['age']
context.country = fieldvalues['country']
return HTTPFound(location=model_url(context, request))
except Invalid, e:
fielderrors = e.error_dict
else:
fieldvalues = context.__dict__
fielderrors = {}
# Render the form and shove some default values in
form_html = render_template(
'templates/myform.pt',
request=request,
formfields=formfields,
fielderrors=fielderrors,
vocabularies=vocabularies,
)
rendered_form = form.merge(form_html, fieldvalues)
return render_template_to_response(
'templates/edit_person.pt',
context = context,
request = request,
page_title='Edit %s' % context.name,
layout=layout,
form_html=rendered_form,
elapsed=form.elapsed,
)
|
Finally the fields in t3/templates/formfields.pt:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal">
<head>
<title>KARL3 Forms Macros</title>
</head>
<body>
<fieldset metal:define-macro="name-field"
tal:define="error fielderrors['name']|None">
<label>Name <span class="required"> </span></label>
<input name="name"/>
<div tal:condition="error" class="error">${error}</div>
</fieldset>
<fieldset metal:define-macro="age-field"
tal:define="error fielderrors['age']|None">
<label>Age <span class="required"> </span></label>
<input name="age"/>
<div tal:condition="error" class="error">${error}</div>
</fieldset>
<fieldset metal:define-macro="country-field"
tal:define="error fielderrors['country']|None">
<label>Country</label>
<select name="country" size="3">
<option value="${c[0]}"
tal:repeat="c vocabularies['countries']">${c[1]}</option>
</select>
<div tal:condition="error" class="error">${error}</div>
</fieldset>
<fieldset metal:define-macro="form-submit">
<input type="submit" name="form.submitted"/>
</fieldset>
</body>
</html>
|
We also made new templates for each the views mentioned. The layout.pt got a better right-column menu, as well as three lines for including jQuery and jQuery UI, plus our own t3.js.