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:
- The authentication user id is provided in the
REMOTE_USER
environment variable and made available in theremote_user
request attribute. - 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 modulebobodoctestumentation.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 thefilter: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 namereload
, as indicated by theuse
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 theconfig_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.
Thefilter-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:
The server calls the debug component.
The debug component calls the who component. If an exception is raised, the
pdb.post_mortem
debugger is invoked.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.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. |