Refactor Into Models

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:

  • Focus on tests. So far the tutorial has taken an approach of

    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.

Fix Tests

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.

Models and Content

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.

  1. 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')
    
  2. 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']
    
  3. 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>
    
  4. 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'
    

Fix Views

We made some changes to both the ZCML, so we need to change the views and view tests.

  1. 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,
            )
    
  2. 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,
            )
    
  3. 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">&nbsp;</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">&nbsp;</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.

Table Of Contents

Previous topic

Speedup, Edit, Lookup

Next topic

More Validators

This Page