[TOC]

A simple wiki

In this section, we build a simple wiki from scratch using only low level APIs (as opposed to using the built-in wiki capabilities of web2py demonstrated in the next section). The visitor will be able to create pages, search them (by title), and edit them. The visitor will also be able to post comments (exactly as in the previous applications), and also post documents (as attachments to the pages) and link them from the pages. As a convention, we adopt the Markmin syntax for our wiki syntax. We will also implement a search page with Ajax, an RSS feed for the pages, and a handler to search the pages via XML-RPC[xmlrpc] . The following diagram lists the actions that we need to implement and the links we intend to build among them.

yUML diagram

Start by creating a new scaffolding app, naming it “mywiki”.

The model must contain three tables: page, comment, and document. Both comment and document reference page because they belong to page. A document contains a file field of type upload as in the previous images application.

Here is the complete model:

db = DAL('sqlite://storage.sqlite')

from gluon.tools import *
auth = Auth(db)
auth.define_tables()
crud = Crud(db)

db.define_table('page',
                Field('title'),
                Field('body', 'text'),
                Field('created_on', 'datetime', default=request.now),
                Field('created_by', 'reference auth_user', default=auth.user_id),
                format='%(title)s')

db.define_table('post',
                Field('page_id', 'reference page'),
                Field('body', 'text'),
                Field('created_on', 'datetime', default=request.now),
                Field('created_by', 'reference auth_user', default=auth.user_id))

db.define_table('document',
                Field('page_id', 'reference page'),
                Field('name'),
                Field('file', 'upload'),
                Field('created_on', 'datetime', default=request.now),
                Field('created_by', 'reference auth_user', default=auth.user_id),
                format='%(name)s')

db.page.title.requires = IS_NOT_IN_DB(db, 'page.title')
db.page.body.requires = IS_NOT_EMPTY()
db.page.created_by.readable = db.page.created_by.writable = False
db.page.created_on.readable = db.page.created_on.writable = False

db.post.body.requires = IS_NOT_EMPTY()
db.post.page_id.readable = db.post.page_id.writable = False
db.post.created_by.readable = db.post.created_by.writable = False
db.post.created_on.readable = db.post.created_on.writable = False

db.document.name.requires = IS_NOT_IN_DB(db, 'document.name')
db.document.page_id.readable = db.document.page_id.writable = False
db.document.created_by.readable = db.document.created_by.writable = False
db.document.created_on.readable = db.document.created_on.writable = False

Edit the controller “default.py” and create the following actions:

  • index: list all wiki pages
  • create: add a new wiki page
  • show: show a wiki page and its comments, and add new comments
  • edit: edit an existing page
  • documents: manage the documents attached to a page
  • download: download a document (as in the images example)
  • search: display a search box and, via an Ajax callback, return all matching titles as the visitor types
  • callback: the Ajax callback function. It returns the HTML that gets embedded in the search page while the visitor types.

Here is the “default.py” controller:

def index():
    """ this controller returns a dictionary rendered by the view
        it lists all wiki pages
    >>> index().has_key('pages')
    True
    """
    pages = db().select(db.page.id, db.page.title, orderby=db.page.title)
    return dict(pages=pages)

@auth.requires_login()
def create():
    """creates a new empty wiki page"""
    form = SQLFORM(db.page).process(next=URL('index'))
    return dict(form=form)

def show():
    """shows a wiki page"""
    this_page = db.page(request.args(0, cast=int)) or redirect(URL('index'))
    db.post.page_id.default = this_page.id
    form = SQLFORM(db.post).process() if auth.user else None
    pagecomments = db(db.post.page_id == this_page.id).select(orderby=db.post.id)
    return dict(page=this_page, comments=pagecomments, form=form)

@auth.requires_login()
def edit():
    """edit an existing wiki page"""
    this_page = db.page(request.args(0, cast=int)) or redirect(URL('index'))
    form = SQLFORM(db.page, this_page).process(
        next = URL('show', args=request.args))
    return dict(form=form)

@auth.requires_login()
def documents():
    """browser, edit all documents attached to a certain page"""
    page = db.page(request.args(0, cast=int)) or redirect(URL('index'))
    db.document.page_id.default = page.id
    grid = SQLFORM.grid(db.document.page_id == page.id, args=[page.id])
    return dict(page=page, grid=grid)

def user():
    return dict(form=auth())

def download():
    """allows downloading of documents"""
    return response.download(request, db)

def search():
    """an ajax wiki search page"""
    return dict(form=FORM(INPUT(_id='keyword',
                                _name='keyword',
                                _onkeyup="ajax('callback', ['keyword'], 'target');")),
                target_div=DIV(_id='target'))

def callback():
    """an ajax callback that returns a <ul> of links to wiki pages"""
    query = db.page.title.contains(request.vars.keyword)
    pages = db(query).select(orderby=db.page.title)
    links = [A(p.title, _href=URL('show', args=p.id)) for p in pages]
    return UL(*links)

Lines 2-6 constitute a comment for the index action. Lines 4-5 inside the comment are interpreted by python as test code (doctest). Tests can be run via the admin interface. In this case the tests verify that the index action runs without errors.

Lines 18, 27, and 35 try to fetch a page record with the id in request.args(0).

Lines 13, 20 define and process create forms for a new page and a new comment and

Line 28 defines and processes an update form for a wiki page.

Line 37 creates a grid object that allows to view, add and update the comments linked to a page.

Some magic happens in line 51. The onkeyup attribute of the INPUT tag “keyword” is set. Every time the visitor releases a key, the JavaScript code inside the onkeyup attribute is executed, client-side. Here is the JavaScript code:

ajax('callback', ['keyword'], 'target');

ajax is a JavaScript function defined in the file “web2py.js” which is included by the default “layout.html”. It takes three parameters: the URL of the action that performs the synchronous callback, a list of the IDs of variables to be sent to the callback ([“keyword”]), and the ID where the response has to be inserted (“target”).

As soon as you type something in the search box and release a key, the client calls the server and sends the content of the ‘keyword’ field, and, when the server responds, the response is embedded in the page itself as the innerHTML of the ‘target’ tag.

The ‘target’ tag is a DIV defined in line 52. It could have been defined in the view as well.

Here is the code for the view “default/create.html”:

{{extend 'layout.html'}}
<h1>Create new wiki page</h1>
{{=form}}

Assuming you are registered and logged in, if you visit the create page, you see the following:

image

Here is the code for the view “default/index.html”:

{{extend 'layout.html'}}
<h1>Available wiki pages</h1>
[ {{=A('search', _href=URL('search'))}} ]<br />
<ul>{{for page in pages:}}
     {{=LI(A(page.title, _href=URL('show', args=page.id)))}}
{{pass}}</ul>
[ {{=A('create page', _href=URL('create'))}} ]

It generates the following page:

image

Here is the code for the view “default/show.html”:

{{extend 'layout.html'}}
<h1>{{=page.title}}</h1>
[ {{=A('edit', _href=URL('edit', args=request.args))}} ]<br />
{{=MARKMIN(page.body)}}
<h2>Comments</h2>
{{for post in comments:}}
  <p>{{=db.auth_user[post.created_by].first_name}} on {{=post.created_on}}
     says <i>{{=post.body}}</i></p>
{{pass}}
<h2>Post a comment</h2>
{{=form}}

If you wish to use markdown syntax instead of markmin syntax:

from gluon.contrib.markdown import WIKI as MARKDOWN

and use MARKDOWN instead of the MARKMIN helper. Alternatively, you can choose to accept raw HTML instead of markmin syntax. In this case you would replace:

{{=MARKMIN(page.body)}}

with:

{{=XML(page.body)}}

(so that the XML does not get escaped, which web2py normally does by default for security reasons).

This can be done better with:

{{=XML(page.body, sanitize=True)}}

By setting sanitize=True, you tell web2py to escape unsafe XML tags such as “