18: Forms and Validation With Deform

Schema-driven, autogenerated forms with validation.

Background

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. This also gives us Colander for schemas and validation.

Deform is getting a facelift, with styling from Twitter Bootstrap and advanced widgets from popular JavaScript projects. The work began in deform_bootstrap and is being merged into an update to Deform.

Objectives

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

Steps

  1. First we copy the results of the view_classes step:

    $ cd ..; cp -r view_classes forms; cd forms
    
  2. Let's edit forms/setup.py to declare a dependency on Deform (which then pulls in Colander as a dependency:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    from setuptools import setup
    
    requires = [
        'pyramid',
        'pyramid_chameleon',
        'deform'
    ]
    
    setup(name='tutorial',
          install_requires=requires,
          entry_points="""\
          [paste.app_factory]
          main = tutorial:main
          """,
    )
    
  3. We can now install our project in development mode:

    $ $VENV/bin/python setup.py develop
    
  4. Register a static view in forms/tutorial/__init__.py for Deform's CSS/JS etc. as well as our demo wikipage scenario's views:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    from pyramid.config import Configurator
    
    
    def main(global_config, **settings):
        config = Configurator(settings=settings)
        config.include('pyramid_chameleon')
        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_static_view('deform_static', 'deform:static/')
        config.scan('.views')
        return config.make_wsgi_app()
    
  5. Implement the new views, as well as the form schemas and some dummy data, in forms/tutorial/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
    import colander
    import deform.widget
    
    from pyramid.httpexceptions import HTTPFound
    from pyramid.view import view_config
    
    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>')
    }
    
    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
    
        @property
        def wiki_form(self):
            schema = WikiPage()
            return deform.Form(schema, buttons=('submit',))
    
        @property
        def reqts(self):
            return self.wiki_form.get_widget_resources()
    
        @view_config(route_name='wiki_view', renderer='wiki_view.pt')
        def wiki_view(self):
            return dict(pages=pages.values())
    
        @view_config(route_name='wikipage_add',
                     renderer='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(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(form=form)
    
        @view_config(route_name='wikipage_view', renderer='wikipage_view.pt')
        def wikipage_view(self):
            uid = self.request.matchdict['uid']
            page = pages[uid]
            return dict(page=page)
    
        @view_config(route_name='wikipage_edit',
                     renderer='wikipage_addedit.pt')
        def wikipage_edit(self):
            uid = self.request.matchdict['uid']
            page = pages[uid]
    
            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(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, form=form)
    
  6. A template for the top of the "wiki" in forms/tutorial/wiki_view.pt:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <title>Wiki: View</title>
    </head>
    <body>
    <h1>Wiki</h1>
    
    <a href="${request.route_url('wikipage_add')}">Add
        WikiPage</a>
    <ul>
        <li tal:repeat="page pages">
            <a href="${request.route_url('wikipage_view', uid=page.uid)}">
                    ${page.title}
            </a>
        </li>
    </ul>
    </body>
    </html>
    
  7. Another template for adding/editing in forms/tutorial/wikipage_addedit.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
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <title>WikiPage: Add/Edit</title>
        <tal:block tal:repeat="reqt view.reqts['css']">
            <link rel="stylesheet" type="text/css"
                  href="${request.static_url(reqt)}"/>
        </tal:block>
        <script src="${request.static_url('deform:static/scripts/jquery-2.0.3.min.js')}"
            type="text/javascript"></script>
        <tal:block tal:repeat="reqt view.reqts['js']">
            <script src="${request.static_url(reqt)}"
                    type="text/javascript"></script>
        </tal:block>
    </head>
    <body>
    <h1>Wiki</h1>
    
    <p>${structure: form}</p>
    <script type="text/javascript">
        deform.load()
    </script>
    </body>
    </html>
    
  8. Finally, a template at forms/tutorial/wikipage_view.pt for viewing a wiki page:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <title>WikiPage: View</title>
    </head>
    <body>
    <a href="${request.route_url('wiki_view')}">
        Up
    </a> |
    <a href="${request.route_url('wikipage_edit', uid=page.uid)}">
        Edit
    </a>
    
    <h1>${page.title}</h1>
    <p>${structure: page.body}</p>
    </body>
    </html>
    
  9. Run your Pyramid application with:

    $ $VENV/bin/pserve development.ini --reload
    
  10. Open http://localhost:6543/ in a 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 packages, Pyramid provides a facility (config.include()) which even makes that unnecessary for consumers of a package. (Deform is not specific to Pyramid.)

Our forms have rich widgets which need the static CSS and JS just mentioned. Deform has a resource registry which allows widgets to specify which JS and CSS are needed. Our wikipage_addedit.pt template shows how we iterated over that data to generate markup that includes the needed resources.

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 a button.

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 succeeded, 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. Give a try at a button that goes to a delete view for a particular wiki page.