Examples

File-system-based wiki

In this section, we present a wiki implementation that stores wiki documents in a file-system directory:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import bobo, os

def config(config):
    global top
    top = config['directory']
    if not os.path.exists(top):
        os.mkdir(top)

edit_html = os.path.join(os.path.dirname(__file__), 'edit.html')

@bobo.query('/')
def index():
    return """<html><head><title>Bobo Wiki</title></head><body>
    Documents
    <hr />
    %(docs)s
    </body></html>
    """ % dict(
        docs='<br />'.join('<a href="%s">%s</a>' % (name, name)
                           for name in sorted(os.listdir(top)))
        )

@bobo.post('/:name')
def save(bobo_request, name, body):
    open(os.path.join(top, name), 'w').write(body)
    return bobo.redirect(bobo_request.path_url, 303)

@bobo.query('/:name')
def get(name, edit=None):
    path = os.path.join(top, name)
    if os.path.exists(path):
        body = open(path).read()
        if edit:
            return open(edit_html).read() % dict(
                name=name, body=body, action='Edit')

        return '''<html><head><title>%(name)s</title></head><body>
        %(name)s (<a href="%(name)s?edit=1">edit</a>)
        <hr />%(body)s</body></html>
        ''' % dict(name=name, body=body)

    return open(edit_html).read() % dict(
        name=name, body='', action='Create')

We need to know the name of the directory to store the files in. On line 3, we define a configuration function, config.

To run this with the bobo server, we’ll use the command line:

bobo -ffswiki.py -cconfig directory=wikidocs

This tells bobo to:

  • run the file fswiki.py
  • pass configuration information to it’s config function on start up, and
  • pass the configuration directory setting of 'wikidocs'.

On line 11, we define an index method to handle / that lists the documents in the wiki.

On line 22, we define a post resource, save, for a post to a named document that saves the body submitted and redirects to the same URL.

On line 27, we define a query, get, for the named document that displays it if it exists, otherwise, it displays a creation page. Also, if the edit form variable is present, an editing interface is presented. By default, queries will accept POST requests, however, because the save function comes first, it is used for POST requests before the get function.

Both the editing and creation interfaces use an edit template, which is just a Python string read from a file that provides a form. In this case, we use Dojo to provide an HTML editor for the body:

<html>
  <head>
    <title>%(action)s %(name)s</title>

    <style type="text/css">
      @import "http://o.aolcdn.com/dojo/1.3.0/dojo/resources/dojo.css";
      @import "http://o.aolcdn.com/dojo/1.3.0/dijit/themes/tundra/tundra.css";
    </style>

    <script
       type="text/javascript"
       src="http://o.aolcdn.com/dojo/1.3.0/dojo/dojo.xd.js.uncompressed.js"
       djConfig="parseOnLoad: true"
       ></script>

    <script type="text/javascript">
      dojo.require("dojo.parser");
      dojo.require("dijit.Editor");
      dojo.require("dijit._editor.plugins.LinkDialog")
      dojo.require("dijit._editor.plugins.FontChoice")

      function update_body() {
          dojo.byId('page_body').value = dijit.byId('editor').getValue();
      }

      dojo.addOnLoad(update_body);
    </script>


  </head>
  <body class="tundra">
    <h1>%(action)s %(name)s</h1>

    <div dojoType="dijit.Editor"
         id="editor"
         onChange="update_body"
         extraPlugins="['insertHorizontalRule', 'createLink',
                        'insertImage', 'unlink', 
                        {name:'dijit._editor.plugins.FontChoice',
                         command:'fontName', generic:true}
                         ]"
         >
      %(body)s
    </div>

    <form method="POST">
      <input type="hidden" name="body" id="page_body">
      <input type="submit" value="Save">
    </form>
  </body>
</html>

File-based wiki with authentication and (minimal) authorization

Traditionally, wikis allowed anonymous edits. Sometimes though, you want to require log in to make changes. In this example, we extend the file-based wiki to require authentication to make changes.

Bobo doesn’t provide any authentication support itself. To provide authentication support for bobo applications, you’ll typically use either an application library, or WSGI middleware. Middleware is attractive because there are a number of middleware authentication implementations available and because authentication is generally something you want to apply in blanket fashion to an entire application.

In this example, we’ll use the repoze.who authentication middleware component, in part because it integrates well using PasteDeploy.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import bobo, os, webob

def config(config):
    global top
    top = config['directory']
    if not os.path.exists(top):
        os.mkdir(top)

edit_html = os.path.join(os.path.dirname(__file__), 'edit.html')

@bobo.query('/login.html')
def login(bobo_request, where=None):
    if bobo_request.remote_user:
        return bobo.redirect(where or bobo_request.relative_url('.'))
    return webob.Response(status=401)

@bobo.query('/logout.html')
def logout(bobo_request, where=None):
    response = bobo.redirect(where or bobo_request.relative_url('.'))
    response.delete_cookie('wiki')
    return response

def login_url(request):
    return request.application_url+'/login.html?where='+request.url

def logout_url(request):
    return request.application_url+'/logout.html?where='+request.url

def who(request):
    user = request.remote_user
    if user:
        return '''
        <div style="float:right">Hello: %s
        <a href="%s">log out</a></div>
        ''' % (user, logout_url(request))
    else:
        return '''
        <div style="float:right"><a href="%s">log in</a></div>
        ''' % login_url(request)

@bobo.query('/')
def index(bobo_request):
    return """<html><head><title>Bobo Wiki</title></head><body>
    <div style="float:left">Documents</div>%(who)s
    <hr style="clear:both" />
    %(docs)s
    </body></html>
    """ % dict(
        who=who(bobo_request),
        docs='<br />'.join('<a href="%s">%s</a>' % (name, name)
                           for name in sorted(os.listdir(top))),
        )

def authenticated(self, request, func):
    if not request.remote_user:
        return bobo.redirect(login_url(request))

@bobo.post('/:name', check=authenticated)
def save(bobo_request, name, body):
    with open(os.path.join(top, name), "wb") as f:
        f.write(body.encode('UTF-8'))
    return bobo.redirect(bobo_request.path_url, 303)

@bobo.query('/:name')
def get(bobo_request, name, edit=None):
    user = bobo_request.remote_user

    path = os.path.join(top, name)
    if os.path.exists(path):
        with open(path, "rb") as f:
            body = f.read().decode("utf-8")
        if edit:
            return open(edit_html).read() % dict(
                name=name, body=body, action='Edit')

        if user:
            edit = ' (<a href="%s?edit=1">edit</a>)' % name
        else:
            edit = ''

        return '''<html><head><title>%(name)s</title></head><body>
        <div style="float:left">%(name)s%(edit)s</div>%(who)s
        <hr style="clear:both" />%(body)s</body></html>
        ''' % dict(name=name, body=body, edit=edit, who=who(bobo_request))

    if user:
        return open(edit_html).read() % dict(
            name=name, body='', action='Create')

    return '''<html><head><title>Not found: %(name)s</title></head><body>
        <h1>%(name)s doesn not exist.</h1>
        <a href="%(login)s">Log in</a> to create it.
        </body></html>
        ''' % dict(name=name, login=login_url(bobo_request))

We’ve added 2 new pages, login.html and logout.html, to our application, starting on line 11.

The login page illustrates 2 common properties of authentication middleware:

  1. The authentication user id is provided in the REMOTE_USER environment variable and made available in the remote_user request attribute.
  2. We signal to middleware that it should ask for credentials by returning a response with a 401 status.

The login method uses remote_user to check whether a user is authenticated. If they are, it redirects them back to the URL from which they were sent to the login page. Otherwise, a 401 response is returned, which triggers repoze.who to present a log in form.

The log out form redirects the user back to the page they came from after deleting the authentication cookie. The authentication cookie is configured in the repoze.who configuration file, who.ini.

We’re going to want most pages to have links to the login and logout pages, and to display the logged in user, as appropriate. We provided some helper functions starting on line 23 for getting log in and log out URLs and for rendering a part of a page that either displays a log in link or the logged-in user and a log out link.

The index function is modified to add the user info and log in or log out links.

The save function illustrates a feature of the query, post, and resource decorators that’s especially useful for adding authorization checks. The save function can’t be used at all unless a user is authenticated. We can pass a check function to the decorator that can compute a response if calling the underlying function isn’t appropriate. In this case, we use an authenticated function that returns a redirect response if a user isn’t authenticated.

The save method is modified to check whether the user is authenticated and to redirect to the login page if they’re not.

The get function is modified to:

  • Display user information and log-in/log-out links
  • Present a not-found page with a log-in link if the page doesn’t exist and the user isn’t logged in.

Some notes about this example:

  • The example implements a very simple authorization model. A user can add or edit content if they’re logged in. Otherwise they can’t.
  • All the application knows about a user is their id. The authentication plug-in passes their log in name as their id. A more sophisticated plug-in would pass a less descriptive identifier and it would be up to the application to look up descriptive information from a user database based on this information.

Assembling and running the example with Paste Deployment and Paste Script

To use WSGI middleware, we’ll use Paste Deployment to configure the middleware and our application and to knit them together. Here’s the configuration file:

[app:main]
use = egg:bobo
bobo_resources = bobodoctestumentation.fswikia
bobo_configure = bobodoctestumentation.fswikia:config
directory = wikidocs
filter-with = reload

[filter:reload]
use = egg:bobo#reload
modules = bobodoctestumentation.fswikia
filter-with = who

[filter:who]
use = egg:repoze.who#config
config_file = who.ini
filter-with = debug

[filter:debug]
use = egg:bobo#debug

[server:main]
use = egg:Paste#http
port = 8080

The configuration defines 5 WSGI components, in 5 sections:

server:main
This section configures a simple HTTP server running on port 8080.
app:main

This section configures our application. The options:

use
The use option instructs Paste Deployment to run the bobo main application.
bobo_resources
The bobo_resources option tells bobo to run the application in the module bobodoctestumentation.fswikia.
bobo_configure
The bobo_configure option tells bobo to call the config function with the configuration options.
directory
The directory option is used by the application to determine where to store wiki pages.
filter-with
The filter-with option tells Paste Deployment to apply the reload middleware, defined by the filter:reload section to the application.
filter:reload

The filter:reload section defines a middleware component that reloads given modules when their sources change. It’s provided by the bobo egg under the name reload, as indicated by the use option.

The filter-with option is used to apply yet another filter, who to the reload middleware.

filter:who

The filter:who section configures a repose.who authentication middleware component. It uses the config_file option to specify a repoze.who configuration file, who.ini:

[plugin:form]
use = repoze.who.plugins.form:make_plugin
login_form_qs = __do_login
rememberer_name = auth_tkt

[plugin:auth_tkt]
use = repoze.who.plugins.auth_tkt:make_plugin
secret = s33kr1t
cookie_name = wiki
secure = False
include_ip = False

[plugin:htpasswd]
use = repoze.who.plugins.htpasswd:make_plugin
filename = htpasswd
check_fn = repoze.who.plugins.htpasswd:crypt_check

[general]
request_classifier = repoze.who.classifiers:default_request_classifier
challenge_decider = repoze.who.classifiers:default_challenge_decider
remote_user_key = REMOTE_USER

[identifiers]
plugins = form;browser auth_tkt

[authenticators]
plugins = auth_tkt htpasswd

[challengers]
plugins = form;browser

See the repoze.who documentation for details of configuring repoze.who.

The filter-with option is used again here to apply a final middleware component, debug.
filter:debug
The filter:debug section defines a post-mortem debugging middleware component that allows us to debug exceptions raised by the application, or by the other 2 middleware components.

In this example, we apply 3 middleware components to the bobo application. When a request comes in:

  1. The server calls the debug component.

  2. The debug component calls the who component. If an exception is raised, the pdb.post_mortem debugger is invoked.

  3. The who component checks for credentials and sets REMOTE_USER in the request environment if they are present. It then calls the reload component. If the response from the reload component has a 401 status, it presents a log in form.

  4. The reload component checks to see if any of it’s configured module sources have changed. If so, it reloads the modules and reinitializes it’s application. (The reload component knows how to reinitialize bobo applications and can only be used with bobo application objects.)

    The reload component calls the bobo application.

The configuration above is intended to support development. A production configuration would omit the reload and debug components:

[app:main]
use = egg:bobo
bobo_resources = bobodoctestumentation.fswikia
bobo_configure = config
directory = wikidocs
filter-with = who

[filter:who]
use = egg:repoze.who#config
config_file = who.ini

[server:main]
use = egg:Paste#http
port = 8080

To run the application in the foreground, we’ll use:

paster serve fswikia.ini

For this to work, the paster script must be installed in such a way that PasteScript, repoze.who, bobo, the wiki application module, and all their dependencies are all importable. This can be done either by installing all of the necessary packages into a (real or virtual) Python, or using zc.buildout.

To run this example, I used a buildout that defined a paste part:

[paste]
recipe = zc.recipe.egg
eggs = PasteScript
       repoze.who
       bobodoctestumentation

The bobodoctestumentation package is a package that includes the examples used in this documentation and depends on bobo. Because the configuration files are in the bobodoctestumentation source directory, I actually ran the application this way:

cd bobodoctestumentation/src/bobodoctestumentation
../../../bin/paster serve fswikia.ini

Ajax calculator

This example shows how the application/json content type can be used in ajax [1] applications. We implement a small (silly) ajax calculator application:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import bobo, os

@bobo.query('/')
def html():
    return open(os.path.join(os.path.dirname(__file__),
                             'bobocalc.html')).read()

@bobo.query(content_type='application/json')
def add(value, input):
    value = int(value)+int(input)
    return dict(value=value)

@bobo.query(content_type='application/json')
def sub(value, input):
    value = int(value)-int(input)
    return dict(value=value)

The html method returns the application page:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<html>
  <head>
    <title>Bobocalc</title>

    <style type="text/css">
      @import "http://o.aolcdn.com/dojo/1.3.0/dojo/resources/dojo.css";
      @import "http://o.aolcdn.com/dojo/1.3.0/dijit/themes/tundra/tundra.css";
    </style>

    <script
       type="text/javascript"
       src="http://o.aolcdn.com/dojo/1.3.0/dojo/dojo.xd.js.uncompressed.js"
       djConfig="parseOnLoad: true, isDebug: true, debugAtAllCosts: true"
       ></script>

    <script type="text/javascript">
      dojo.require("dojo.parser");
      dojo.require("dijit.form.Button");
      dojo.require("dijit.form.ValidationTextBox");

      bobocalc = function () {
          function op(url) {
              dojo.xhrGet({
                  url: url, handleAs: 'json',
                  content: {
                      value: dojo.byId('value').textContent,
                      input: dijit.byId('input').value
                  },
                  load: function(data) {
                      dojo.byId('value').textContent = data.value;
                      dojo.byId('input').value = '';
                  }
              });
          }
          return {
              add: function () { op('add.json'); },
              sub: function () { op('sub.json'); },
              clear: function () { dojo.byId('value').textContent = 0; }
          };
      }();
    </script>

  </head>
  <body class="tundra">
    <h1><em>Bobocalc</em></h1>

    Value: <span id="value">0</span>
    <form>
      <label for="input">Input:</label>
      <input
         type="text" id="input" name="input"
         dojoType="dijit.form.ValidationTextBox" regExp="[0-9]+"
         />
      <button dojoType="dijit.form.Button" onClick="bobocalc.clear">C</button>
      <button dojoType="dijit.form.Button" onClick="bobocalc.add">+</button>
      <button dojoType="dijit.form.Button" onClick="bobocalc.sub">-</button>
    </form>
  </body>
</html>

This page presents a value, and input field and clear (C), add (+) and subtract (-) buttons. When the user selects the add or subtract buttons, an ajax request is made to the server. The ajax request passes the input and current value as form data to the add or sub resources on the server.

The add and sub methods in bobocalc.py simply convert their arguments to integers and compute a new value which they return in a dictionary. Because we used the application/json content type, the dictionaries returned are marshaled as JSON.

Static resources

We provide a resource that serves a static file-system directory. This is useful for serving static resources such as javascript source and CSS.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import bobo, mimetypes, os, webob

@bobo.scan_class
class Directory:

    def __init__(self, root, path=None):
        self.root = os.path.abspath(root)+os.path.sep
        self.path = path or root

    @bobo.query('')
    def base(self, bobo_request):
        return bobo.redirect(bobo_request.url+'/')

    @bobo.query('/')
    def index(self):
        links = []
        for name in sorted(os.listdir(self.path)):
            if os.path.isdir(os.path.join(self.path, name)):
                name += '/'
            links.append('<a href="%s">%s</a>' % (name, name))
        return """<html>
        <head><title>%s</title></head>
        <body>
          %s
        </body>
        </html>
        """ % (self.path[len(self.root):], '<br>\n  '.join(links))

    @bobo.subroute('/:name')
    def traverse(self, request, name):
        path = os.path.abspath(os.path.join(self.path, name))
        if not path.startswith(self.root):
            raise bobo.NotFound
        if os.path.isdir(path):
            return Directory(self.root, path)
        else:
            return File(path)

@bobo.scan_class
class File:
    def __init__(self, path):
        self.path = path

    @bobo.query('')
    def base(self, bobo_request):
        response = webob.Response()
        content_type = mimetypes.guess_type(self.path)[0]
        if content_type is not None:
            response.content_type = content_type
        try:
            with open(self.path, "rb") as f:
                response.body = f.read()
        except IOError:
            raise bobo.NotFound

        return response

This example illustrates:

traversal
The Directory.traverse method enables directories to be traversed with a name to get to sub-directories or files.
use of the bobo.NotFound exception
Rather than construct a not-found ourselves, we simply raise bobo.NotFound, and let bobo generate the response for us.

[1]This isn’t strictly “Ajax”, because there’s no XML involved. The requests we’re making are asynchronous and pass data as form data and generally expect response data to be formatted as JSON.