Wiki Models with SQLAlchemy
I was working on an example application for the upcoming Werkzeug release and decided to make a wiki for that purpose. (The main reason is that I found the Creoleparser on the pypi index and thought that it would integrate into a genshi powered wiki well)
Now wikis have an interesting data structure. Basically you have pages and revisions, where a revision is bound to exactly one page. Simple to model and my database definition looks like that:
page_table = Table('pages', metadata,
Column('page_id', Integer, primary_key=True),
Column('name', String(60), unique=True)
)
revision_table = Table('revisions', metadata,
Column('revision_id', Integer, primary_key=True),
Column('page_id', Integer, ForeignKey('pages.page_id')),
Column('timestamp', DateTime),
Column('text', String),
Column('change_note', String(200))
)
Very simple schema and you can easily bind it to two classes:
class Revision(object):
def __init__(self, page, text, change_note='', timestamp=None):
if isinstance(page, (int, long)):
self.page_id = page
else:
self.page = page
self.text = text
self.change_note = change_note
self.timestamp = timestamp or datetime.utcnow()
def render(self, request=None):
"""Render the page text into a genshi stream."""
if request is None:
request = get_request()
if request is None:
raise RuntimeError('rendering requires request context')
return parse_creole(request, self.text)
class Page(object):
def __init__(self, name):
self.name = name
@property
def title(self):
return self.name.replace('_', ' ')
Session.mapper(Revision, revision_table)
Session.mapper(Page, page_table, properties=dict(
revisions=relation(Revision, backref='page', order_by=[desc(Revision.revision_id)])
))
Works pretty well but as soon as you start putting your stuff into the template it feels unpythonic :-) You basically have to provide always two objects, the revision and the page. Why not combine that into one model? First I removed my database tables again and combined that into one table but then I found out that you can map joins to classes in SQLAlchemy. That and Python’s ability to do multiple inheritance gives me the ability to combine both tables into one class:
class RevisionedPage(Page, Revision):
pass
Session.mapper(RevisionedPage, join(page_table, revision_table), properties=dict(
page_id=[page_table.c.page_id, revision_table.c.page_id],
))
Very nice for the templates and also easy to query. It looks like a normal python class. I haven’t tried if that also works for write access, but I doubt it. Because of that I have added an exception to the __init__ method to avoid creating pages and revisions via the RevisionedPage object.
The full example can be found in the simplewiki sources.
Hi, any special reason not to use Jinja for the templates in this example ?
Comment by k4ml — Saturday, November 24th, 2007 @ 6:20 amFirst of all creoleparser uses genshi internally so it plays nice with genshi itself of course. On the other hand I want to show that you can combine werkzeug with any library out there not just the pocoo libraries. :-)
Regards,
Comment by Armin Ronacher — Saturday, November 24th, 2007 @ 7:01 pmArmin
Thank you for writing this bit, it does help conceptualising the thing. I saw how Django does that, I read the definitions of Active Record and Data Mapper, and your piece gives a clear Python example on how to do the second with SQLAlchemy.
Don’t want to say a word on Wikicreole. Rather writing reStructuredText alone than struggling with brackets! End of sarcasm :) Best regards, Wok
Comment by Wok — Saturday, January 5th, 2008 @ 6:32 am