9: Forms and Validation With Deform

Modern web applications deal extensively with forms. Developers, though, have a wide range of philosophies about how frameworks should help them with their forms. As such, Pyramid doesn’t directly bundle one particular form library. Instead, there are a variety of form libraries that are easy to use in Pyramid.

Deform is one such library. In this step, we introduce Deform for our forms and validation.

Objectives

  • Make a schema using Colander, the companion to Deform
  • Create a form with Deform and change our views to handle validation

Steps

  1. Let’s use the previous package as a starting point for a new distribution. Also, use easy_install to install Deform:

    (env33)$ cd ..; cp -r step08 step09; cd step09
    (env33)$ easy_install-3.3 deform
    (env33)$ python3.3 setup.py develop
    
  2. Deform has CSS and JS that help make it look pretty. Change the tutorial/__init__.py to add a static view for Deform’s static assets:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    from pyramid.config import Configurator
    
    
    def main(global_config, **settings):
        config = Configurator(settings=settings)
        config.add_route('wiki_view', '/')
        config.add_route('wikipage_add', '/add')
        config.add_route('wikipage_view', '/{uid}')
        config.add_route('wikipage_edit', '/{uid}/edit')
        config.add_route('wikipage_delete', '/{uid}/delete')
        config.add_static_view(name='static', path='tutorial:static')
        config.add_static_view('deform_static', 'deform:static/')
        config.scan()
        return config.make_wsgi_app()
    
  3. To keep our dummy data out of our views.py (and pave the way for a future step that does modeling), let’s move pages to tutorial/models.py:

    pages = {
        '100': dict(uid='100', title='Page 100', body='<em>100</em>'),
        '101': dict(uid='101', title='Page 101', body='<em>101</em>'),
        '102': dict(uid='102', title='Page 102', body='<em>102</em>')
    }
    
  4. Our tutorial/views.py has some significant changes. The add and edit views handle both GET and POST (form submission), we have methods, and most of all, a form schema for WikiPage:

      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
    import colander
    import deform.widget
    
    from pyramid.decorator import reify
    from pyramid.httpexceptions import HTTPFound
    from pyramid.renderers import get_renderer
    from pyramid.view import view_config
    
    from .models import pages
    
    
    class WikiPage(colander.MappingSchema):
        title = colander.SchemaNode(colander.String())
        body = colander.SchemaNode(
            colander.String(),
            widget=deform.widget.RichTextWidget()
        )
    
    
    class WikiViews(object):
        def __init__(self, request):
            self.request = request
            renderer = get_renderer("templates/layout.pt")
            self.layout = renderer.implementation().macros['layout']
    
        @reify
        def wiki_form(self):
            schema = WikiPage()
            return deform.Form(schema, buttons=('submit',))
    
        @reify
        def reqts(self):
            return self.wiki_form.get_widget_resources()
    
        @view_config(route_name='wiki_view',
                     renderer='templates/wiki_view.pt')
        def wiki_view(self):
            return dict(title='Welcome to the Wiki',
                        pages=pages.values())
    
        @view_config(route_name='wikipage_add',
                     renderer='templates/wikipage_addedit.pt')
        def wikipage_add(self):
            form = self.wiki_form.render()
    
            if 'submit' in self.request.params:
                controls = self.request.POST.items()
                try:
                    appstruct = self.wiki_form.validate(controls)
                except deform.ValidationFailure as e:
                    # Form is NOT valid
                    return dict(title='Add Wiki Page', form=e.render())
    
                # Form is valid, make a new identifier and add to list
                last_uid = int(sorted(pages.keys())[-1])
                new_uid = str(last_uid + 1)
                pages[new_uid] = dict(
                    uid=new_uid, title=appstruct['title'],
                    body=appstruct['body']
                )
    
                # Now visit new page
                url = self.request.route_url('wikipage_view', uid=new_uid)
                return HTTPFound(url)
    
            return dict(title='Add Wiki Page', form=form)
    
        @view_config(route_name='wikipage_view',
                     renderer='templates/wikipage_view.pt')
        def wikipage_view(self):
            uid = self.request.matchdict['uid']
            page = pages[uid]
            return dict(page=page, title=page['title'])
    
        @view_config(route_name='wikipage_edit',
                     renderer='templates/wikipage_addedit.pt')
        def wikipage_edit(self):
            uid = self.request.matchdict['uid']
            page = pages[uid]
            title = 'Edit ' + page['title']
    
            wiki_form = self.wiki_form
    
            if 'submit' in self.request.params:
                controls = self.request.POST.items()
                try:
                    appstruct = wiki_form.validate(controls)
                except deform.ValidationFailure as e:
                    return dict(title=title, page=page, form=e.render())
    
                # Change the content and redirect to the view
                page['title'] = appstruct['title']
                page['body'] = appstruct['body']
    
                url = self.request.route_url('wikipage_view',
                                             uid=page['uid'])
                return HTTPFound(url)
    
            form = wiki_form.render(page)
    
            return dict(page=page, title=title, form=form)
    
        @view_config(route_name='wikipage_delete')
        def wikipage_delete(self):
            uid = self.request.matchdict['uid']
            del pages[uid]
    
            url = self.request.route_url('wiki_view')
            return HTTPFound(url)
    
  5. We don’t want to include the Deform JS/CSS in every page. We thus need a “slot” in tutorial/templates/layout.pt into which we can insert these static assets:

     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
    <!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:metal="http://xml.zope.org/namespaces/metal"
          xmlns:tal="http://xml.zope.org/namespaces/tal"
          metal:define-macro="layout">
    <head>
        <title>Wiki - ${title}</title>
        <more metal:define-slot="head-more"></more>
        <link rel="stylesheet"
              href="${request.static_url('tutorial:static/wiki.css')}"/>
    </head>
    <body>
    <div id="main">
        <h1>
            <a href="${request.route_url('wiki_view')}">
                <img src="${request.static_url('tutorial:static/logo.png')}"
                     alt="Logo"/></a>
            ${title}</h1>
    
        <div metal:define-slot="content">
        </div>
    </div>
    </body>
    </html>
    
  6. tutorial/templates/wikipage_addedit.pt needs to iterate over the resources and insert them in the slot we just made, as well as insert the rendered form:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <div metal:use-macro="view.layout">
        <more tal:omit-tag="" metal:fill-slot="head-more">
            <tal:block repeat="reqt view.reqts['css']">
                <link rel="stylesheet" type="text/css"
                      href="${request.static_url('deform:static/' + reqt)}"/>
            </tal:block>
            <tal:block repeat="reqt view.reqts['js']">
                <script src="${request.static_url('deform:static/' + reqt)}"
                        type="text/javascript"></script>
            </tal:block>
        </more>
        <div metal:fill-slot="content">
            <p>${structure: form}</p>
            <script type="text/javascript">
                deform.load()
            </script>
        </div>
    </div>
    
  7. Run the tests in your package using nose:

    (env33)$ nosetests .
    ..
    -----------------------------------------------------------------
    Ran 2 tests in 1.971s
    
    OK
    
  8. Run the WSGI application:

    (env33)$ pserve development.ini --reload
    
  9. Open http://127.0.0.1:6547/ in your browser.

Analysis

This step helps illustrate the utility of asset specifications for static assets. We have an outside package called Deform with static assets which need to be published. We don’t have to know where on disk it is located. We point at the package, then the path inside the package.

We just need to include a call to add_static_view to make that directory available at a URL. For Pyramid-specific pages, Pyramid provides a facility (config.include()) which even makes that unnecessary for consumers of a package. (Deform is not specific to Pyramid.)

Our add and edit views use a pattern called self-posting forms. Meaning, the same URL is used to GET the form as is used to POST the form. The route, the view, and the template are the same whether you are walking up to it the first time or you clicked “submit”.

Inside the view we do if 'submit' in self.request.params: to see if this form was a POST where the user clicked on a particular button <input name="submit">.

The form controller then follows a typical pattern:

  • If you are doing a GET, skip over and just return the form
  • If you are doing a POST, validate the form contents
  • If the form is invalid, bail out by re-rendering the form with the supplied POST data
  • If the validation succeeeded, perform some action and issue a redirect via HTTPFound.

We are, in essence, writing our own form controller. Other Pyramid-based systems, including pyramid_deform, provide a form-centric view class which automates much of this branching and routing.

Extra Credit

  1. Do I have to publish my Deform static assets at the /deform_static/ URL path? What happens if I change it? (Give this a try by editing deform_static in tutorial/__init__.py.)

  2. Analyze the following and discern what is the intention:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @view_defaults(route_name='wikipage_edit',
                   renderer='templates/wikipage_addedit.pt')
    class WikiPageViews(object):
    
        def __init__(self, request):
            self.request = request
    
        @view_config(request_param='form.update')
        def wikipage_update(self):
            # some work
            return dict(title="Form Update")
    
        @view_config(request_param='form.draft')
        def wikipage_draft(self):
            # some work
            return dict(title="Form Draft")
    
        @view_config(request_param='form.delete')
        def wikipage_delete(self):
            # some work
            return dict(title="Form Delete")
    

Table Of Contents