building web pages with python manipulating data

Manipulating the Data

Viewing Comparisons and Different Formats

Now that there's the ability to Add, Edit, and View revisions to the data, we can start performing some cool tricks with it.

First the ability to compare Revisions will be added. This can be done with the HTMLDiff module. When given two pieces of text, HTMLDiff will locate the differences between the two and surround them with <del></del> and <ins></ins> tags.

Download the HTMLDiff module and add it to the list of modules in base.py:

import diff

Creating the Compare Template

The first step will be to create the compare template. In tpl, create a new file called compare.html:

{% extends "base" %}
{% marker "title" set "View All" %}

{% block "content" %}
    <h2>{{ n.note_title }}</h2>
    <h3>{{ n.category_category }}</h3>

    <div class="txt">{{ diffText }}</div>
{% endblock %}

{% block "sidebar" %}
    <div id="sidebar">
        <h3>Revision Compare:</h3>
        <ul class="rev">
            <li>First: {{ r1.revision_time }}</li>
            <li>Second: {{ r2.revision_time }}</li>
        </ul>

        <ul class="back">
            <li><a href="/view/note/{{ n.note_id }}">back</a></li>
        </ul>
    </div>
{% endblock %}

Creating the Compare Controller

Next it's time to create the compare controller. In controllers, create a new file called compare.py.

from base import *

class note(baseWebpy):
    def GET(self, n, id1, id2):
        if n and noteExists(n) and id1 and revisionExists(id1) and id2 and revisionExists(id2):
            note = getNotes(n)[0]
            r1 = getRevision(revision_id=id1)
            r2 = getRevision(revision_id=id2)
            diffText = diff.textDiff(r1['revision_body'], r2['revision_body'])
            diffText = markdown(diffText)
            return sendData('compare', {
                'n': note,
                'r1': r1,
                'r2': r2,
                'diffText': diffText,
            })
        else:
            raise PageNotFound

The GET method of this class accepts three parameters: a note_id and two revision_ids. The Note is retrieved like normal, as are the two Revisions. Next, the two Revisions are compared and the result is stored in diffText. diffText is then converted to Markdown. Finally, all variables are sent to the template and printed out.

Also remember to add compare.py to __init__.py.

Modifying the getRevision() Function

The astute reader will notice a change in the getRevision() function. Normally, we pass a note_id to get the revision history. However, this time we're passing a revision_id. This will simply return the row of the ID we asked for. The change to the function in models.py is simple:

def getRevision(note_id="", revision_id=""):
    if note_id:
        return select([revision],
            revision.c.note_note_id==note_id,
            order_by=[desc(revision.c.revision_time)]).execute().fetchall()
    if revision_id:
        return select([revision], revision.c.revision_id==revision_id).execute().fetchone()

Along with the two named function parameters, an if block has also been added to the end.

Adding the Compare URLs

The final step, like usual is to add the entries for compare to the urls map:

        (r'^compare/(\d*)/(\d*)/(\d*)/$', controllers.compare.note),

Comparing Notes

The Compare feature is now set up and ready to use. Start the Colubrid server and give it a shot. You'll notice that links for the Compare feature were added way back when we built the viewNote template.

Viewing Notes in Different Formats

One of the best features about using a Smarty / Django / Jinja style template engine is the ability to output the text in any format you want easily. Currently, the Notes are all being viewed in XHTML, but we're now going to add the ability to view them in plain text.

Creating the Plain Text Template

The plain-text template will be called viewTXT.html. I realize that having a .html extension for a plain-text template is rather a misnomer, but just ignore it.

Title: {{ note.note_title }}
Category: {{ note.category_category }}
Last Update: {{ note.revision_time }}

{{ note.revision_body }}

Modifying the viewNote Template

In order to see the plain-text version, we need a link to take us there. Change the viewNote template to the following:

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

{% block "content" %}
<h2>{{ note.note_title }}</h2>
<h3>{{ note.category_category }}</h3>
{{ note.revision_body }}
{% endblock %}

{% block "sidebar" %}
<div id="sidebar">
<h3>Revisions</h3>
<ul>
{% for r in revisions %}
    <li>
        (<a href="/view/txt/{{ note.note_id }}/{{ r.revision_id }}">txt</a>)
        <a href="/view/note/{{ note.note_id }}/{{ r.revision_id }}">{{ r.revision_time }}</a>
        {% if not loop.first %} 
            &nbsp; &nbsp; &nbsp;
            <a href="/compare/{{ note.note_id }}/{{ r.revision_id }}/{{ note.revision_id }}">Compare</a>
        {% endif %}
    </li>
{% endfor %}
</ul>
<h3>Functions</h3>
<ul class="functions">
    <li class="edit"><a href="/edit/note/{{ note.note_id }}">Edit this note</a></li>
    <li class="delete"><a href="/delete/note/{{ note.note_id }}">Delete this note</a></li>
</ul>
</div>
{% endblock %}

Modifying the View Controller

Since viewing -- whether it be XHTML or plain-text -- falls under the view action, we'll be placing the class in the view controller. It looks something like this:

class txt(baseWebpy):
    def GET(self, note_id, revision_id=""):
        if note_id and noteExists(note_id):
            if revision_id and not revisionExists(revision_id):
                raise PageNotFound
            else:
                tpl = 'viewTXT'
                note = getNotes(note_id, revision_id)[0]
                return sendData(tpl, {
                        'note': note
                }, 'text/plain')
        else:
            raise PageNotFound

Very similar to the normal note class. The only changes are which template to use and the 'text/plain' content type. Remember back when the sendData() function was originally created, we added support to specify what Content-Type we'd like to see. Now it comes in handy.

Adding the Plain-text URLs

Now for the URL mappings:

(r'^view/txt/(\d*)/$', controllers.view.txt),
(r'^view/txt/(\d*)/(\d*)/$', controllers.view.txt),

Just like the /view/note mappings, we have the ability to see either an up-to-date Note or a specific Revision of a note.

Checkpoint

controllers/__init__.py
import view
import addEdit
import compare
controllers/compare.py
from base import *

class note(baseWebpy):
    def GET(self, n, id1, id2):
        if n and noteExists(n) and id1 and revisionExists(id1) and id2 and revisionExists(id2):
            note = getNotes(n)[0]
            r1 = makeWritable(getRevision(revision_id=id1))
            r2 = makeWritable(getRevision(revision_id=id2))
            diffText = diff.textDiff(r1['revision_body'], r2['revision_body'])
            diffText = markdown(diffText)
            return sendData('compare', {
                'n': note,
                'r1': r1,
                'r2': r2,
                'diffText': diffText,
            })
        else:
            raise PageNotFound
controllers/view.py
from base import *

class index(baseWebpy):
    def GET(self):
        return sendData('index', {
            'cat': getCategories(),
            'note': getNotes()
        })

class note(baseWebpy):
    def GET(self, note_id, revision_id=""):
        if note_id and noteExists(note_id):
            if revision_id and not revisionExists(revision_id):
                raise PageNotFound
            else:
                tpl = 'viewNote'
                note = makeWritable(getNotes(note_id, revision_id)[0])
                note['revision_body'] = markdown(note['revision_body'])
                return sendData(tpl, {
                    'note': note,
                    'revisions': getRevision(note_id=note_id)
                })
        else:
            raise PageNotFound

class txt(baseWebpy):
    def GET(self, note_id, revision_id=""):
        if note_id and noteExists(note_id):
            if revision_id and not revisionExists(revision_id):
                raise PageNotFound
            else:
                tpl = 'viewTXT'
                note = getNotes(note_id, revision_id)[0]
                return sendData(tpl, {
                        'note': note
                }, 'text/plain')
        else:
            raise PageNotFound
tpl/compare.html
{% extends "base" %}
{% marker "title" set "View All" %}

{% block "content" %}
    <h2>{{ n.note_title }}</h2>
    <h3>{{ n.category_category }}</h3>

    <div class="txt">{{ diffText }}</div>
{% endblock %}

{% block "sidebar" %}
    <div id="sidebar">
        <h3>Revision Compare:</h3>
        <ul class="rev">
            <li>First: {{ r1.revision_time }}</li>
            <li>Second: {{ r2.revision_time }}</li>
        </ul>

        <ul class="back">
            <li><a href="/view/note/{{ n.note_id }}">back</a></li>
        </ul>
    </div>
{% endblock %}
tpl/viewTXT.html
Title: {{ note.note_title }}
Category: {{ note.category_category }}
Last Update: {{ note.revision_time }}

{{ note.revision_body }}
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'^view/note/(\d*)/$', controllers.view.note),
        (r'^view/note/(\d*)/(\d*)/$', controllers.view.note),
        (r'^add/note/$', controllers.addEdit.note),
        (r'^edit/note/(\d*)/$', controllers.addEdit.note),
        (r'^compare/(\d*)/(\d*)/(\d*)/$', controllers.compare.note),
        (r'^view/txt/(\d*)/$', controllers.view.txt),
        (r'^view/txt/(\d*)/(\d*)/$', controllers.view.txt),
    ]

    slash_append = True

app = Notebook
app = StaticExports(app, {
    '/static': './static'
})
if __name__ == '__main__':
        execute(reload=True, debug=False)
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(category_id=""):
    r = select([category])
    return r.execute().fetchall()


def getNotes(note_id="", revision_id=""):
        r = select([note,category], 
            and_(note.c.category_category_id==category.c.category_id))
        if note_id:
            r = select([note,category,revision],
                and_(note.c.category_category_id==category.c.category_id,
                     note.c.note_id==revision.c.note_note_id,
                     note.c.note_id==note_id),
                     order_by=[desc(revision.c.revision_time)], limit=1,)
            if revision_id:
                r.append_whereclause(revision.c.revision_id==revision_id)

        return r.execute().fetchall()

def addEditNote(formData):
    try:
        c = db.connect()
        transaction = c.begin()
        if formData.has_key('note_id'):
            formData['note_note_id'] = formData['note_id']
            c.execute(note.update(note.c.note_id==formData['note_id']), formData)
        else:
            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

def noteExists(note_id):
    n = note.select(note.c.note_id==note_id).execute().fetchall()
    if n:
        return True
    else:
        return False

def revisionExists(revision_id):
    r = revision.select(revision.c.revision_id==revision_id).execute().fetchall()
    if r:
        return True
    else:
        return False

def categoryExists(category_id):
    c = category.select(category.c.category_id==category_id).execute().fetchall()
    if c:
        return True
    else:
        return False

def getRevision(note_id="", revision_id=""):
    if note_id:
        return select([revision],
            revision.c.note_note_id==note_id,
            order_by=[desc(revision.c.revision_time)]).execute().fetchall()
    if revision_id:
        return select([revision], revision.c.revision_id==revision_id).execute().fetchone()