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 %}
<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()
