Simple Validation

In this first step we make a view with a self-posting form, but no machinery at all. No validators, no majik. Instead, we show:

  • In KARL, if the machinery ever gets in the way, just do things manually.
  • A brief introduction to lxml.html and its form-filling capability.

Getting Started

  1. cd to the t3 directory, the one containing setup.py.

  2. Edit setup.py and say that this package uses nose as its test runner. Do this by replacing the existing test_suite line with the following:

    test_suite="nose.collector",
    

    KARL3 uses nose as its test suite runner.

  3. Make sure the tests still run. From the top t3 directory:

    (t3)bash-3.2$ python ./setup.py test
    running test
    running egg_info
    writing requirements to t3.egg-info/requires.txt
    writing t3.egg-info/PKG-INFO
    writing top-level names to t3.egg-info/top_level.txt
    writing dependency_links to t3.egg-info/dependency_links.txt
    writing entry points to t3.egg-info/entry_points.txt
    reading manifest file 't3.egg-info/SOURCES.txt'
    writing manifest file 't3.egg-info/SOURCES.txt'
    running build_ext
    test_my_view (t3.tests.ViewIntegrationTests) ... ok
    test_my_view (t3.tests.ViewTests) ... ok
    
    Ran 2 tests in 0.243s
    
    OK
    
  4. Add some error logging middleware in your t3.ini file:

    [DEFAULT]
    debug = true
    
    [app:zodb]
    use = egg:FeedsTool#app
    reload_templates = true
    debug_authorization = false
    debug_notfound = false
    zodb_uri = file://%(here)s/Data.fs?connection_cache_size=20000
    
    [pipeline:main]
    pipeline =
        egg:Paste#cgitb
        egg:repoze.zodbconn#closer
        egg:repoze.tm#tm
        zodb
    
    [server:main]
    use = egg:Paste#http
    host = 0.0.0.0
    port = 6543
  5. Run the server:

    (t3)bash-3.2$ paster serve t3.ini --reload
    
  6. Visit http://localhost:6543/ in your browser. You should see the colorful BFG intro screen.

Make a Macro

To shorten the ZPT, let’s have a “theme”.

  1. Create a t3/templates/layout.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
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    <!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" 
          metal:define-macro="master">
     <head>
       <title>KARL3 Forms Demo</title>
       <link href="${request.application_url}/static/default.css" 
    	 rel="stylesheet" type="text/css" />
       <link href="${request.application_url}/static/t3.css" 
    	 rel="stylesheet" type="text/css" />
     </head>
     <body>
       <div id="logo">
         <h2><code>KARL3 Forms</code>, a <code>repoze.bfg</code> application</h2>
       </div>
       <div id="header">
         <div id="menu">
         </div>
       </div>
       <div id="wrapper">
         <!-- start page -->
         <div id="page">
    
           <!-- start content -->
           <div class="post">
    	 <h1 class="title">Sample Form</h1>
    	 <div metal:define-slot="content" id="content"/>
           </div>
           <!-- end content -->
    
           <!-- start sidebar -->
           <div id="sidebar">
    	 <ul>
    	   <li>
    	     <a href="/">Home</a>
    	   </li>
    	   <li>
    	     <a href="/add_feed.html">Add Feed</a>
    	   </li>
    	 </ul>
           </div>
           <!-- end sidebar -->
           <div style="clear: both;"></div>
         </div>
       </div>
    
       <!-- end sidebar -->
       <div style="clear: both;"></div>
       <div id="footer">
         <p id="legal">
           ( c ) 2008. All Rights Reserved. Template design
    	 by <a href="http://www.freecsstemplates.org/">Free CSS
    	 Templates</a>.<br />  The form part of this view renders at
    	 ${elapsed} requests/sec.
         </p>
       </div>
    
     </body>
    </html>
    

    Note

    We include a spot in the footer to measure the performance of the form system part of the request.

  2. This theme includes a new t3/templates/static/t3.css:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    .error {
        margin-left: 3em;
        color: yellow;
    }
    
    .required {
        background-color: red;
        padding-left: 0.4em;
    }
    
  3. Change t3/templates/mytemplate.pt to fill the slot:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    <div xmlns="http://www.w3.org/1999/xhtml" 
         xmlns:tal="http://xml.zope.org/namespaces/tal"
         xmlns:metal="http://xml.zope.org/namespaces/metal"
         metal:use-macro="layout.macros['master']">
    
      <div metal:fill-slot="content">
    
        ${form_html}
    
      </div>
    
    </div>
    
  4. We render the form separately from the page so we can use lxml.html to hack it. The form is in t3/templates/myform.pt:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    <html xmlns:tal="http://xml.zope.org/namespaces/tal">
    
      <form method="post" action="${request.url}" class="${formerror}">
        <fieldset>
          <label>Age <span class="required">&nbsp;</span></label>
          <input name="age"/>
          <div tal:condition="fielderror" class="error">${fielderror}</div>
        </fieldset>
        <fieldset>
          <input type="submit" name="form.submitted"/>
        </fieldset>
      </form>
    
    </html>
    
    • Visually marking something “required” is simply a matter of markup. No HTML generation.
    • We include a conditional spot to put an error message for the field.
  5. The t3/views.py file does more work:

     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
    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 lxml.html import document_fromstring
    from lxml.html import tostring
    from time import time
    
    static_view = static('templates/static')
    
    def my_view(context, request):
        layout = get_template('templates/layout.pt')
    
        start = time()
    
        fielderror = formerror = None
        default_age = formfill_age = 40
        max_age = 200
    
        if request.POST.get('form.submitted', False):
            # Form was submitted, do some validation
            age = request.POST.get('age', False)
            if age is False or age == '':
                print 'Age is required'
                fielderror = 'Age is required'
            try:
                if int(age) > max_age:
                    print '%s is over the max_age of %s' % (age, max_age)
                    fielderror = 'Age must not be over %s' % age
                # Whether age is valid or not, shove it back into the form
                formfill_age = age
            except ValueError:
                print 'Age is not an integer'
                fielderror = 'Age must be an integer'
                formfill_age = age
    
        form_html = render_template(
            'templates/myform.pt',
            request=request,
            formerror=formerror,
            fielderror=fielderror,
            )
    
        # Merge in the value for the age, either from the default or from
        # what they typed in.
        form_tree = document_fromstring(form_html)
        form = form_tree.forms[0]
        form.fields['age'] = str(formfill_age)
        merged_form_html = tostring(form)
    
        elapsed = str(1 / (time() - start))[0:7]
    
        return render_template_to_response(
            'templates/mytemplate.pt',
            request = request,
            layout=layout,
            form_html=merged_form_html,
            elapsed=elapsed,
            )
    
    • Load the “layout” and pass it into the renderer.
    • Set formerror and fielderror to None by default, then make sure to pass it into the renderer.
    • Choose a default_age of 40 and a max_age of 200. Set the default as the value we plan to shove into the form later using lxml.html.
    • Check to see if form.submitted is in the POST data. If so, the form was submitted.
    • Grab the value of age. We want to consider this a required field. So if it is missing or empty, flag an error.
    • Apply some primitive validation tests to age. Keep track of why it fails. Make sure to set the value we plan to shove into the form to what the user typed in.
    • Grab the myform.pt form definition and render it.
    • Use lxml.html and its form machinery to parse the form, do some form-filling, and serialize the result.
  6. Go to http://localhost:6543/ in your browser and you should see the form.

Table Of Contents

Previous topic

Tutorial Installation

Next topic

First Base Class

This Page