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