building web pages with python viewing a note

Viewing a Note

Details on Viewing

Now that we have the ability to add Notes to the Notebook, we need a way to view them. It's relatively simple and it'll be set up in no time.

Besides viewing the note, there will be a couple extra features. On each Note page, there will also be a list of all past revisions made. The revisions will be clickable -- meaning we'll be able to go back and see the whole Note as it was in the past. Secondly, functions to Edit and Delete the Note as well as Compare the current revision to past revisions will also be available. These functions will be added in later Parts.

Creating the Template

Let's start with the template. Create a new file in tpl called viewNote.html. It'll look something like this:

{% 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/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 %}

The content block is where the Note's information and its current revision will be displayed. The sidebar block will contain two features which we'll use in the future: a list of revisions and the ability to edit and delete the note.

Modifying the View Controller

Since we already have a view.py controller, we'll just be adding on to it. Currently, there's just an index class. To view the note, we'll add a note class:

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

Just like any other new class that gets added, there are a few new functions to go over. This time it's noteExists(), revisionExists(), makeWritable(), getRevisions(), and markdown(). Additionally, getNotes() is also used, but it's been modified.

Creating the New Model Functions

noteExists() and revisionExists() are two simple functions that will just return True or False if the item exists or not. This is to just prevent people from blindly typing in numbers.

They're extremely small and simple functions:

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

While we're at it, we might as well add a categoryExists() function, too:

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

Finally, a getRevisions() function is made to get all the Revisions for a specified note_id:

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

Editing the getNotes() Function:

Now it's time to modify getNotes() to be able to accept two parameters: a note_id and a revision_id. Now the function will be able to return three different items. If no parameters are given, all Notes are returned. If a note_id is given, the Note plus it's current Revision is returned. Finally, if both a note_id and a revision_id are given, the Note with its corresponding Revision is returned.

SQLAlchemy makes doing this very simple by allowing us to append to already made queries. The new getNotes() will look like this:

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

See how all that's really been added is the if block and the corresponding SQLAlchemy appends.

Regardless on if there's only a single entry to return, getNotes() is still going to return a list. This is just so the code doesn't have become complicated by supporting several different return types. This is why there's a [0] at the end of the function call in our new note class.

Supporting Markdown

A nice feature I wanted to add to Notebook was to be able to type my text in Markdown. To do this, the Markdown module is needed and it also needs imported. Simply add

from markdown import *

to the list of modules in base.py.

Creating the makeWritable() function

SQLAlchemy returns a datatype that can be used as both an object and a list. This datatype is also read-only. So when the text is converted to Markdown, a new variable has to be made.

Incase I wanted to be able to edit more than just the Revision body, I decided to write a function that will make the whole SQLAlchemy result writable. That's where the makeWritable() function comes in. Inside utils.py, add:

def makeWritable(r):
    w = {}
    for x in r.keys():
        w[x] = r[x]
    return w

This returns a dictionary thats writable and we're now able to convert the text to Markdown and save it in the same Key as before.

Supporting Page not Found

When there's an error -- such as a specified Note cannot be found, we have the ability to throw a 404 page. To do this, simply add:

from colubrid.exceptions import PageNotFound

to base.py

Adding the New URLs

The final step is to add support for the new URLs:

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

This supports the ability to just view one note:

/view/note/1

And the ability to view a note and a specific revision:

/view/note/1/3

As a side note, I know for sure there's a way to combine these into one line. If anyone wants to take a shot at it, please do.

Viewing the Note

And that's it. Start up the built-in Colubrid server and view your note!

Checkpoint

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
controllers/base.py
from colubrid.exceptions import PageNotFound
from utils import *
from models import *
from forms import *
from markdown import *

class baseWebpy:
    def __init__(self):
        session.clear()
tpl/viewNote.html
{% 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/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 %}
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

def makeWritable(r):
    w = {}
    for x in r.keys():
        w[x] = r[x]
    return w
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),
        (r'^view/note/(\d*)/$', controllers.view.note),
        (r'^view/note/(\d*)/(\d*)/$', controllers.view.note),
    ]

    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 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

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=""):
    if note_id:
        return select([revision],
            revision.c.note_note_id==note_id,
            order_by=[desc(revision.c.revision_time)]).execute().fetchall()