building web pages with python adding a note

Adding a Note

How Notes will be Added

The process of adding a Note to the application is going to include a couple steps. First, a form will have to be presented to the user. The form will be written in standard XHTML, but it will be in a file of its own. This is so FormEncode can process only the form information and nothing else on the page.

FormEncode will take the users input and check it for validity. This can include things like maximum and minimum length, data types, etc.

After the information is found to be valid, it'll be passed to SQLAlchemy. SQLAlchemy will start a transaction, then insert a row into Note. Once the row is inserted, the newly created note_id will be retrieved and used to insert a row into Revision -- which contains the actual body of the Note.

Creating the Form

Create a new file in the tpl directory called addNoteForm.html. Our form isn't going to be very complicated at all:

<form name="add" method="post" action=".">
     <label for="category_category_id">Category</label>
     <select name="category_category_id">
         {% for c in cats %}
             <option value="{{ c.category_id }}">{{ c.category_category }}</option>
         {% endfor %}
     </select>
     <br/>
     <label for="note_title">Title</label><br/>
     <input type="text" name="note_title" id="note_title" size="45"/>
     <br/>
     <label for="revision_body">Body</label><br/>
     <textarea rows="23" cols="75" name="revision_body" id="revision_body"></textarea>
     <br/>
     <br/>
     <input type="Submit" name="Submit" />
</form>

As you can see, it includes three form items: a textbox for the title, a select box for the category, and a textarea for the body of the note.

The select box will be built from a query to the Category table.

Next, create a second file called addNote.html. This will include the rest of the form page:

{% extends "base" %}
{% marker "title" set "Add Note" %}

{% block "content" %}

    {{ form }}

{% endblock %}

{{ form }} is just going to end up being the actual form shown above.

Processing the Form with FormEncode

Next, we'll set up how the form will actually be processed. For this, create a new file called forms.py in the root of notebook.demo.

The contents will be:

from formencode import schema, validators, htmlform
from formencode.api import Invalid
from utils import *
import models

class addNoteSchema(schema.Schema):
    allow_extra_fields = True
    note_title = validators.String(not_empty=True)
    revision_body = validators.String(not_empty=True)
    category_category_id = validators.Int(not_empty=True)

def createAddNoteForm(formData={}):
    tpl = tplLoad('addNoteForm', {
        'cats': models.getCategories()
    })
    form = htmlform.HTMLForm(tpl, addNoteSchema())
    return form.render(formData)

def validateAddNoteForm(formData):
    tpl = createAddNoteForm()
    form = htmlform.HTMLForm(tpl, addNoteSchema())
    results, errors = form.validate(formData)
    if errors:
        return form.render(formData, errors)
    else:
        return 0

From the top, forms.py is importing the needed FormEncode modules as well as our utilities from utils.py and the models module. Note how models is being imported -- this because both FormEncode and SQLAlchemy have schema keywords. There needs to be a name separation between the two so there isn't a name clash.

The addNoteSchema class is similar to the SQLAlchemy schema we made earlier. This tells FormEncode what kind of textfields it can expect to see and what datatypes are valid. Notice the allow_extra_fields setting. This tells FormEncode that extra fields (such as the Submit button) are OK to be in the form.

The first function, createAddNoteForm() works similar to the tplLoad() function. It takes an optional dictionary of data -- which will come in handy when we create the Edit capability. First, the function will generate the XHTML form using the tplLoad() function. Next, it creates a form instance that combines the XHTML with the Form Schema. Finally, it returns the completely rendered XHTML form.

The validateAddNoteForm() function starts out the exact same way as the previous function. The only differnece is that it will call the validate() method to check for errors. If there are errors, it renders the XHTML form, but includes error messages for the user.

Inserting the Valid Data

Once the form data is found to be valid, we can insert it into database. To do that, a new function will be created in models.py called addNote():

def addNote(formData):
    try:
        c = db.connect()
        transaction = c.begin()
        c.execute(note.insert(), formData)
        newNote = c.execute(note.select(order_by=[desc(note.c.note_id)], limit=1)).fetchone()
        formData['note_note_id'] = newNote['note_id']
        c.execute(revision.insert(), formData)
        session.flush()
        transaction.commit()
        c.close()
        return formData['note_note_id']
    except:
        transaction.rollback()
        c.close()
        raise

addNote() takes the valid form data in the format of a dictionary. Next, it creates a direct connection to the database and stores it in c. A transaction is created next from the c variable.

note.insert() is a built-in SQLAlchemy function that calls the INSERT function on the Note table. For the values, it will use the formData dictionary. Once the row is inserted, a quick SELECT is executed to get the new note_id. Once that's found, a new key is added to formData. This key is the Foreign Key, note_note_id, that will store the note reference for the Revision table.

A second INSERT function is called. This time for Revision. Notice how the same formData dictionary is used. That's because formData contains information for both tables. This is why it's important that all the column names are unique -- or else data from one table will also go into another. SQLAlchemy is smart enough to use only the keys in the dictionary that are also the names of the columns in the table.

Once the Revision is inserted, the session is flushed. This basically forces all changes to be made to the database. Once the flush is made, the transaction is commited. The function ends with returning the new note_id.

The whole function is wrapped in a try / except block. If at any time something in the try block fails, the transaction is rolled back and an error is raised.

Gluing it all Together

We have the actual XHTML form, the functions to validate the form, and the functions to insert the data into the database. Now it's time to glue it all together and show it to the user. Create a new controller called add.py and insert the following:

from base import *

class note(baseWebpy):
    def GET(self):
        return sendData('addNote', {
            'form': createAddNoteForm()
        })

    def POST(self):
        i = formData(self.request.form)
        eForm = validateAddNoteForm(i)
        if eForm:
            return sendData('addNote', {
                'form': eForm
            })
        else:
            try:
                note_id = addNote(i)
                return HttpResponse('Note %s has been added!' % note_id)
            except Exception, e:
                return HttpResponse('uh oh!')

For the GET method, we're simply rendering the form and sending it to the user. POST has a lot more action, though.

The first thing to notice with is the new formData() function. Don't get this confused with the formData variable we used in models.py. Colubrid allows for multiple values for a form element in a datatype known as a MultiDict. More information on that can be found here. Because of that, the POST data might have multiple values when we only want one. formData basically eliminates the multiple values and only uses one. In essense, it converts MultiDict to just Dict. Place the following in utils.py:

def formData(dirty):
    if dirty:
        clean = {}
        for k in dirty.keys():
            clean[k] = dirty.getlist(k)[0]
        return clean

Next, POST validates the form data with the validateAddNoteForm() function. If the form has errors, the error form is displayed. If not, a try / except block is started and the addNote() model function is called. If addNote() is successful, it returns the new note_id which is then displayed on the screen.

Don't forget to add import add to your __init__.py file and from forms import * to your base.py controller. Also, add the following to the urls map:

(r'^add/note/$', controllers.add.note),

After you have all of this, you should now be able to run the built-in Colubrid server, click on the New Note link and add a note. Give it a shot! Once your done, check out the index page -- your new Note is automatically listed.

A Caveat with FormEncode

When using FormEncode, you'll notice the following error:

FormEncode-0.5.1-py2.4.egg/formencode/htmlform.py:42: 
DeprecationWarning: HTMLForm has been deprecated; 
use the htmlfill and htmlfill_schemabuilder modules directly. 
stacklevel=1)

This obviously means that the way I'm using FormEncode is deprecated. However, the only documentation I found on how to use FormEncode used this method.

If anyone can fill me in on how to use the new method, please do. I tried reading through the code, but admittedly, failed. Or, if you know of a better way to process forms, please share!

Checkpoint

controllers/__init__.py
import view
import add
controllers/add.py
from base import *

class note(baseWebpy):
    def GET(self):
        return sendData('addNote', {
            'form': createAddNoteForm()
        })

    def POST(self):
        i = formData(self.request.form)
        eForm = validateAddNoteForm(i)
        if eForm:
            return sendData('addNote', {
                'form': eForm
            })
        else:
            try:
                note_id = addNote(i)
                return HttpResponse('Note %s has been added!' % note_id)
            except Exception, e:
                return HttpResponse('uh oh!')
forms.py
from formencode import schema, validators, htmlform
from formencode.api import Invalid
from utils import *
import models

class addNoteSchema(schema.Schema):
    allow_extra_fields = True
    note_title = validators.String(not_empty=True)
    revision_body = validators.String(not_empty=True)
    category_category_id = validators.Int(not_empty=True)

def createAddNoteForm(formData={}):
    tpl = tplLoad('addNoteForm', {
        'cats': models.getCategories()
    })
    form = htmlform.HTMLForm(tpl, addNoteSchema())
    return form.render(formData)

def validateAddNoteForm(formData):
    tpl = createAddNoteForm()
    form = htmlform.HTMLForm(tpl, addNoteSchema())
    results, errors = form.validate(formData)
    if errors:
        return form.render(formData, errors)
    else:
        return 0
utils.py
from jinja import Template, Context, FileSystemLoader
from colubrid import HttpResponse

def tplLoad(tpl, vars={}):
    t = Template(tpl, FileSystemLoader('tpl'))
    c = Context(vars)
    return t.render(c)

def sendData(tpl, vars, ctype='text/html', code=200):
    page = tplLoad(tpl, vars)
    return HttpResponse(page, [('Content-Type', ctype)], code)

def formData(dirty):
    if dirty:
        clean = {}
        for k in dirty.keys():
            clean[k] = dirty.getlist(k)[0]
        return clean
models.py
from sqlalchemy import *

db = create_engine('sqlite:///sql/notebook.db')

metadata = BoundMetaData(db)

category = Table('category', metadata, 
    Column('category_id', Integer, primary_key=True),
    Column('category_category', String(255)),
)

note = Table('note', metadata, 
    Column('note_id', Integer, primary_key=True),
    Column('note_title', String(255)),
    Column('category_category_id', Integer, ForeignKey('category.category_id')),
)

revision = Table('revision', metadata, 
    Column('revision_id', Integer, primary_key=True),
    Column('note_note_id', Integer, ForeignKey('note.note_id')),
    Column('revision_body', String),
    Column('revision_time', DateTime, default=func.current_timestamp()),
)

session = create_session()

def getCategories():
    r = select([category])
    return r.execute().fetchall()


def getNotes():
        r = select([note,category], 
            and_(note.c.category_category_id==category.c.category_id))
        return r.execute().fetchall()

def addNote(formData):
    try:
        c = db.connect()
        transaction = c.begin()
        c.execute(note.insert(), formData)
        newNote = c.execute(note.select(order_by=[desc(note.c.note_id)], limit=1)).fetchone()
        formData['note_note_id'] = newNote['note_id']
        c.execute(revision.insert(), formData)
        session.flush()
        transaction.commit()
        c.close()
        return formData['note_note_id']
    except:
        transaction.rollback()
        c.close()
        raise
app.py
from colubrid import WebpyApplication, Request, execute, HttpResponse
from colubrid.server import StaticExports
import controllers

class Notebook(WebpyApplication):
    urls = [
        (r'^$', controllers.view.index),
        (r'^add/note/$', controllers.add.note),
    ]

    slash_append = True

app = Notebook
app = StaticExports(app, {
    '/static': './static'
})
if __name__ == '__main__':
        execute(reload=True, debug=False)
tpl/addNote.html
{% extends "base" %}
{% marker "title" set "Add Note" %}

{% block "content" %}

    {{ form }}

{% endblock %}
tpl/addNoteForm.html
`<form name="add" method="post" action=".">
     <label for="category_category_id">Category</label>
     <select name="category_category_id">
         {% for c in cats %}
             <option value="{{ c.category_id }}">{{ c.category_category }}</option>
         {% endfor %}
     </select>
     <br/>
     <label for="note_title">Title</label><br/>
     <input type="text" name="note_title" id="note_title" size="45"/>
     <br/>
     <label for="revision_body">Body</label><br/>
     <textarea rows="23" cols="75" name="revision_body" id="revision_body"></textarea>
     <br/>
     <br/>
     <input type="Submit" name="Submit" />
</form>`