Templates

Formats for Web Service Responses

In a web service, you are free to return whatever set of responses formats you so choose; however, plain text, JSON, and HTML are probably the most common

FormatHuman-friendlyComputer-friendly
HTMLYesYes, if structured
JSONNoYes
text/plainYesMaybe

Often, a web service may convey information in more than one format. (Example: /portfolio/process.gv.txt, /portfolio/process.gv.txt?format=raw, /portfolio/process.gv.txt?format=svg; you could also use the Accept: header, but you can't make a permalink this way ) (Another example: /my-service/foo.json, /my-service/foo.txt, /my-service/foo.html, etc.)

python and JSON

JSON == JavaScript Object Notation

Returning JSON in python is easy:

>>> import json
>>> foo = { 'hello': 1, 'foobar': 2, 'fleem': {'nested': True, 'error': None}}
>>> json.dumps(foo)
'{"fleem": {"error": null, "nested": true}, "foobar": 2, "hello": 1}'

In a web service, you would just return this as the response.

In recent versions of python, the json module is part of the standard library. In older versions, you will have to easy_install simplejson

How to support both json and simplejson:

try:
  import json
except ImportError:
  import simplejson as json

You should also add simplejson to the install_requires section of your setup.py

Of course, you can only use json.dumps() to serialize objects that are supported in JSON, such as strings, numbers, dictionaries, arrays, True, False, and None. Try to do this with other objects will result in an error.

>>> import sys
>>> json.dumps(sys.stdin)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.6/json/__init__.py", line 230, in dumps
    return _default_encoder.encode(obj)
  File "/usr/lib/python2.6/json/encoder.py", line 367, in encode
    chunks = list(self.iterencode(o))
  File "/usr/lib/python2.6/json/encoder.py", line 317, in _iterencode
    for chunk in self._iterencode_default(o, markers):
  File "/usr/lib/python2.6/json/encoder.py", line 323, in _iterencode_default
    newobj = self.default(o)
  File "/usr/lib/python2.6/json/encoder.py", line 344, in default
    raise TypeError(repr(o) + " is not JSON serializable")
TypeError: <open file '<stdin>', mode 'r' at 0xb78c2020> is not JSON serializable

Loading JSON is doable with json.loads, but it is picky

What is a template?

A template is a prototype of a document that will be rendered with variables (and possibly logic) to create a final document.

How to create a python package

Let's say you have a python file, that you want to package. We'll use a8e.py from last class. But before we start ...

Why bother creating a python package?

If you're just going to run a script from the command line, then you might not want to package it. However, making a real package has certain advantages:

So how do I create one?

PasteScript comes with a basic_package template out of the box. Once you easy_install PasteScript, it should be available to you.

Making a8e.py into a package:

  1. paster create a8e
  2. fill out necessary details
  3. cp /path/to/a8e.py a8e/a8e/ # could also override a8e/a8e/__init__
  4. cd a8e; python setup.py develop # in a virtualenv
  5. your module is now importable:
    >>> from a8e import a8e
    >>> a8e.__file__
    '/home/jhammel/stage/src/a8e/a8e/a8e.py'
    >>> dir(a8e)
    ['__builtins__', '__doc__', '__file__', '__name__', '__package__', 'a8e', 'main', 'sys', 'urllib2']
    >>> a8e.a8e('hello')
    'h3o'
    >>> 
        
  6. (yes, that's a lot of a8e's)

The filesystem layout looks something like this:

a8e/
|-- a8e
|   |-- a8e.py
|   `-- __init__.py
|-- setup.cfg
`-- setup.py

(and the setup.cfg is pretty optional)

webob_view: a PasteScript template

It is easier to start a new project from an example.

http://k0s.org/hg/webob_view/

Installing webob_view:

# create a virtualenv if you haven't
hg clone http://k0s.org/hg/webob_view/
cd webob_view
python setup.py develop

Listing available PasteScript templates:

(stage)> paster create --list-templates
Available templates:
  basic_package:   A basic setuptools-enabled package
  command_script:  pastescript template for creating command line
  applications
  console_script:  pastescript template for creating command line
  applications
  genshi_view:     a simple view with webob + genshi
  paste_deploy:    A web application deployed through paste.deploy
  webob_view:      a simple view with webob

Creating a new project with webob_view :

(stage)> paster create -t webob_view hello
Selected and implied templates:
  webob-view#webob_view  a simple view with webob

Variables:
  egg:      hello
  package:  hello
  project:  hello
Enter description (One-line description of the package) ['']: a
  description
Enter author (Author name) ['']: Jeff Hammel
Enter author_email (Author email) ['']: jhammel@mozilla.com
Enter url (URL of homepage) ['']: http://k0s.org/
Enter port (port to serve paste) ['']: 7654
Creating template webob_view
Creating directory ./hello
  Recursing into +package+
    Creating ./hello/hello/
    Copying __init__.py to ./hello/hello/__init__.py
    Copying dispatcher.py to ./hello/hello/dispatcher.py
    Copying factory.py_tmpl to ./hello/hello/factory.py
    Copying handlers.py to ./hello/hello/handlers.py
  Copying +package+.ini_tmpl to ./hello/hello.ini
  Copying README.txt_tmpl to ./hello/README.txt
  Copying setup.py_tmpl to ./hello/setup.py
Running /home/jhammel/stage/bin/python setup.py egg_info

Serving the created project:

cd hello 
python setup.py develop
(stage)> paster serve hello.ini 
Starting server in PID 2050.
serving on 0.0.0.0:7654 view at http://127.0.0.1:7654

Viewing the page:

> curl http://127.0.0.1:7654
<html><body><form method="post">Hello,
<input type="text" name="name" value="world"/></form></body></html>

Or use your browser!

What does webob_view give you?

When you render the webob_view template, what do you get out of it?

Being a template, you are allowed -- nay, encouraged! -- to alter any of the resultant code (pylons has a similar philosophy)

Web templates

Why web templates?

Python web templates:

Templates are typically used by passing in variables. In python, this is usually a dict.

Most template flavors allow simple display-oriented logic (django's does not)

The bad way of doing things

If you don't use templates, your code ends up looking like this:

def application(environ, start_response):
  """a simple application to alphabetize words"""
  request = Request(environ)
  words = sorted(request.GET.keys())
  variables = { 'title': request.GET.get('title', 'Hello World'),
                'body': '<li>' + '</li><li>'.join(words) + '</li>'
                }
  template = """<html><head><title>%(title)s</title><body><h1>%(title)s</h1><div><ul>%(body)s</ul></div></body></html>""" % variables
  response = Response(content_type='text/html',
                      template)
  return response(environ, start_response)

Genshi templates

Genshi is an XML and text templating language that focuses on robustness and streams

The previous example:

from genshi.template import TemplateLoader
loader = TemplateLoader('/path/to/directory')
def application(environ, start_response):
  """a simple application to alphabetize words"""
  request = Request(environ)
  words = sorted(request.GET.keys())
  variables = { 'title': request.GET.get('title', 'Hello World'),
                'words': words
                }
  template = self.loader.load('alphabetize.html')
  content = template.genereate(**variables).render()
  response = Response(content_type='text/html',
                      content)
  return response(environ, start_response)

The template:

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:py="http://genshi.edgewall.org/">
  <head>
    <title>${title}</title>
  </head>
  <body>
    <h1>${title}</h1>
    <div>
      <ul>
        <li py:for="word in words">${word}</li>
      </ul>
    </div>
  </body>
</html>

alphabetize.html

Example: the class homepage

Remember the crazy URL that made the page black and blink weird?

http://k0s.org/mozilla/craft/?show_header=Accept,Host,Unicorn&blink=true&black#end

This is all done with a genshi template. The WebOb Request object is passed in. From there, the template does the rest.

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:py="http://genshi.edgewall.org/"
      xmlns:xi="http://www.w3.org/2001/XInclude">
  <head>
    <title>Beginning Python Web Services</title>
    <link rel="stylesheet" type="text/css" href="/css/professional.css" title="Default"/>
    <style type="text/css">
      body { 
      width: 800px;
      alignment: center;
      margin-left: auto;
      margin-right: auto;
      }
    </style>
  </head>
  <body py:attrs="'black' in request.GET and {'bgcolor': 'black'} or {}">

    <xi:include href="site-nav.html"/>

    <h1>
      <a href="http://p2pu.org/webcraft/beginning-python-webservices"
         title="p2pu course page">
        <blink py:strip="request.GET.get('blink') != 'true'">
          Beginning Python Web Services
        </blink>
      </a>
    </h1>

    <h2>Material</h2>
    <ul id="listing">
      <li><a href="introduction.html">Introduction</a></li>
      <li><a href="http.html">HTTP</a></li>
      <li><a href="project.html">Project</a></li>
      <li><a href="python.html">Python and the Web</a></li>
      <li><a href="templates.html">Templates</a></li>
      <li><a href="middleware.html">Middleware</a></li>
      <li><a href="deployment.html">Deployment</a></li>
      <li><a href="architecture.html">Web Service and Web Site Architecture</a></li>
      <li><a href="email.html">Email Services</a></li>
    </ul>

    <h2><a href="questions.html">Homework questions</a></h2>

    <div py:if="'show_header' in request.GET"
         py:with="headers = request.GET['show_header'] and request.GET['show_header'].split(',') or sorted(request.headers.keys())">
      <div py:for="header in headers">
        <a href="http://www.google.com/search?q=${header}+HTTP+header">
          <i py:strip="header in request.headers">
            ${'%s: %s' % (header, request.headers.get(header, 'The request did not contain header "%s"' % header))}
          </i>
        </a>
      </div>
    </div>

    <a name="end">The End</a>
  </body>
</html>

http://k0s.org/mozilla/craft/index.html?format=raw

genshi_view: webob_view with Genshi templates

genshi_view extends webob_view with genshi templates

Other features over webob_view:

You may not need these for every project, but they're easy to delete and they might be useful

Yes, this is a system of (web) templates in a (file) template

Genshi example project: SimpleWiki

Steps:
  1. invoke the genshi_view template
  2. add a genshi template renderer
  3. add directory listings
  4. add a POST request handler
  5. add an edit view

Step: Invoke genshi_view

(stage)> paster create -t genshi_view SimpleWiki
Selected and implied templates:
  genshi-view#genshi_view  a simple view with webob + genshi

Variables:
  egg:      SimpleWiki
  package:  simplewiki
  project:  SimpleWiki
Enter description (One-line description of the package) ['']: an example wiki with genshi
Enter author (Author name) ['']: Jeff Hammel
Enter author_email (Author email) ['']: jhammel@mozilla.com
Enter url (URL of homepage) ['']: http://k0s.org/mozilla/craft/
Enter port (port to serve paste) ['']: 12345
Creating template genshi_view
Creating directory ./SimpleWiki
  Recursing into +package+
    Creating ./SimpleWiki/simplewiki/
    Copying __init__.py to ./SimpleWiki/simplewiki/__init__.py
    Copying dispatcher.py to ./SimpleWiki/simplewiki/dispatcher.py
    Copying factory.py_tmpl to ./SimpleWiki/simplewiki/factory.py
    Copying handlers.py to ./SimpleWiki/simplewiki/handlers.py
    Recursing into static
      Creating ./SimpleWiki/simplewiki/static/
      Copying jquery.js to ./SimpleWiki/simplewiki/static/jquery.js
    Recursing into templates
      Creating ./SimpleWiki/simplewiki/templates/
      Copying index.html to ./SimpleWiki/simplewiki/templates/index.html
      Copying navigation.html to ./SimpleWiki/simplewiki/templates/navigation.html
  Copying +package+.ini_tmpl to ./SimpleWiki/simplewiki.ini
  Copying README.txt_tmpl to ./SimpleWiki/README.txt
  Copying setup.py_tmpl to ./SimpleWiki/setup.py
Running /home/jhammel/stage/bin/python setup.py egg_info
(stage)> cd SimpleWiki/
(stage)> python setup.py develop
running develop
running egg_info
writing requirements to SimpleWiki.egg-info/requires.txt
writing SimpleWiki.egg-info/PKG-INFO
writing top-level names to SimpleWiki.egg-info/top_level.txt
writing dependency_links to SimpleWiki.egg-info/dependency_links.txt
writing entry points to SimpleWiki.egg-info/entry_points.txt
writing requirements to SimpleWiki.egg-info/requires.txt
writing SimpleWiki.egg-info/PKG-INFO
writing top-level names to SimpleWiki.egg-info/top_level.txt
writing dependency_links to SimpleWiki.egg-info/dependency_links.txt
writing entry points to SimpleWiki.egg-info/entry_points.txt
reading manifest file 'SimpleWiki.egg-info/SOURCES.txt'
writing manifest file 'SimpleWiki.egg-info/SOURCES.txt'
running build_ext
Creating /home/jhammel/stage/lib/python2.6/site-packages/SimpleWiki.egg-link (link to .)
Adding SimpleWiki 0.0 to easy-install.pth file

Installed /home/jhammel/stage/src/SimpleWiki
Processing dependencies for SimpleWiki==0.0
Searching for Genshi==0.6
Best match: Genshi 0.6
Processing Genshi-0.6-py2.6.egg
Genshi 0.6 is already the active version in easy-install.pth

Using /home/jhammel/stage/lib/python2.6/site-packages/Genshi-0.6-py2.6.egg
Searching for PasteScript==1.7.3
Best match: PasteScript 1.7.3
Processing PasteScript-1.7.3-py2.6.egg
PasteScript 1.7.3 is already the active version in easy-install.pth
Installing paster script to /home/jhammel/stage/bin
Installing paster script to /home/jhammel/stage/bin

Using /home/jhammel/stage/lib/python2.6/site-packages/PasteScript-1.7.3-py2.6.egg
Searching for Paste==1.7.3.1
Best match: Paste 1.7.3.1
Processing Paste-1.7.3.1-py2.6.egg
Paste 1.7.3.1 is already the active version in easy-install.pth

Using /home/jhammel/stage/lib/python2.6/site-packages/Paste-1.7.3.1-py2.6.egg
Searching for WebOb==0.9.8
Best match: WebOb 0.9.8
Processing WebOb-0.9.8-py2.6.egg
WebOb 0.9.8 is already the active version in easy-install.pth

Using /home/jhammel/stage/lib/python2.6/site-packages/WebOb-0.9.8-py2.6.egg
Searching for PasteDeploy==1.3.3
Best match: PasteDeploy 1.3.3
Processing PasteDeploy-1.3.3-py2.6.egg
PasteDeploy 1.3.3 is already the active version in easy-install.pth

Using /home/jhammel/stage/lib/python2.6/site-packages/PasteDeploy-1.3.3-py2.6.egg
Finished processing dependencies for SimpleWiki==0.0
(stage)>

Step: add a Genshi template renderer

diff --git a/example/hello.html b/example/hello.html
new file mode 100644
--- /dev/null
+++ b/example/hello.html
@@ -0,0 +1,8 @@
+<html>
+<head>
+<title>Hello, world!</title>
+</head>
+<body>
+Hello, world!
+</body>
+</html>
diff --git a/simplewiki.ini b/simplewiki.ini
--- a/simplewiki.ini
+++ b/simplewiki.ini
@@ -14,9 +14,9 @@ port = 12345
 [composite:main]
 use = egg:Paste#urlmap
 / = SimpleWiki
 
 set debug = false
 
 [app:SimpleWiki]
 paste.app_factory = simplewiki.factory:factory
-SimpleWiki.name = world
\ No newline at end of file
+SimpleWiki.directory = %(here)s/example
\ No newline at end of file
diff --git a/simplewiki/dispatcher.py b/simplewiki/dispatcher.py
--- a/simplewiki/dispatcher.py
+++ b/simplewiki/dispatcher.py
@@ -1,43 +1,41 @@
 """
 request dispatcher:
 data persisting across requests should go here
 """
 
 import os
 
-from handlers import Index
+from handlers import GenshiRenderer
 
 from genshi.template import TemplateLoader
 from paste.fileapp import FileApp
 from pkg_resources import resource_filename
 from webob import Request, Response, exc
 
 class Dispatcher(object):
 
     ### class level variables
     defaults = { 'auto_reload': 'False',
                  'template_dirs': '',
-                 'app': None,
-                 'name': 'anonymous' }
+                 'name': 'anonymous',
+                 'directory': None }
 
     def __init__(self, **kw):
 
         # set instance parameters from kw and defaults
         for key in self.defaults:
             setattr(self, key, kw.get(key, self.defaults[key]))
         self.auto_reload = self.auto_reload.lower() == 'true'
 
+        assert self.directory and os.path.exists(self.directory), "Must specify an existing directory"
+
         # request handlers
-        self.handlers = [ Index ]
-
-        # endpoint app if used as middleware
-        if self.app:
-            assert hasattr(self.app, '__call__')
+        self.handlers = [ GenshiRenderer ]
 
         # template loader
         self.template_dirs = self.template_dirs.split()
         self.template_dirs.append(resource_filename(__name__, 'templates'))
         self.loader = TemplateLoader(self.template_dirs,
                                      auto_reload=self.auto_reload)
 
     def __call__(self, environ, start_response):
@@ -52,18 +50,16 @@ class Dispatcher(object):
         request.environ['path'] = path
 
         # match the request to a handler
         for h in self.handlers:
             handler = h.match(self, request)
             if handler is not None:
                 break
         else:
-            if self.app:
-                return self.app(environ, start_response)
             handler = exc.HTTPNotFound
 
         # add navigation links to handler [example]
         if hasattr(handler, 'data'):
             handler.data.setdefault('links', [])
             for h in self.handlers:
                 handler.data['links'].append((handler.link(h.handler_path), 
                                               h.__name__))
diff --git a/simplewiki/handlers.py b/simplewiki/handlers.py
--- a/simplewiki/handlers.py
+++ b/simplewiki/handlers.py
@@ -1,13 +1,14 @@
 """
 request handlers:
 these are instantiated for every request, then called
 """
 
+import os
 from urlparse import urlparse
 from webob import Response, exc
 
 class HandlerMatchException(Exception):
     """the handler doesn't match the request"""
 
 class Handler(object):
 
@@ -44,38 +45,47 @@ class Handler(object):
         else:
             application_url = [ self.application_path ]
         path = application_url + path
         return '/'.join(path)
 
     def redirect(self, location):
         raise exc.HTTPSeeOther(location=location)
 
-class GenshiHandler(Handler):
+class GenshiRenderer(Handler):
+
+    @classmethod
+    def match(cls, app, request):
+
+        # check the method
+        if request.method not in cls.methods:
+            return None
+
+        # check the path
+        path = request.environ['path']
+        if not path:
+            return None
+        if not path[-1].endswith('.html'):
+            return None
+
+        
+        try:
+            return cls(app, request)
+        except HandlerMatchException:
+            return None
 
     def __init__(self, app, request):
         Handler.__init__(self, app, request)
+        self.template = os.path.join(app.directory, *request.environ['path'])
+        if not os.path.exists(self.template):
+            raise HandlerMatchException
         self.data = { 'request': request,
                       'link': self.link }
 
     def __call__(self):
         return getattr(self, self.request.method.title())()
 
     def Get(self):
         # needs to have self.template set
         template = self.app.loader.load(self.template)
         return Response(content_type='text/html',
                         body=template.generate(**self.data).render('html'))
 
-class Index(GenshiHandler):
-    template = 'index.html'
-    methods=set(['GET', 'POST'])
-
-    def __init__(self, app, request):
-        GenshiHandler.__init__(self, app, request)
-
-    def Get(self):
-        self.data['name'] = self.request.remote_user or self.app.name
-        return GenshiHandler.Get(self)
-
-    def Post(self):
-        self.app.name = self.request.POST.get('name', self.app.name)
-        self.redirect(self.link(self.handler_path))

changes to add renderer

diff format

Step: add a directory index handler

diff --git a/simplewiki/dispatcher.py b/simplewiki/dispatcher.py
--- a/simplewiki/dispatcher.py
+++ b/simplewiki/dispatcher.py
@@ -1,16 +1,16 @@
 """
 request dispatcher:
 data persisting across requests should go here
 """
 
 import os
 
-from handlers import GenshiRenderer
+from handlers import GenshiRenderer, Index
 
 from genshi.template import TemplateLoader
 from paste.fileapp import FileApp
 from pkg_resources import resource_filename
 from webob import Request, Response, exc
 
 class Dispatcher(object):
 
@@ -25,17 +25,17 @@ class Dispatcher(object):
         # set instance parameters from kw and defaults
         for key in self.defaults:
             setattr(self, key, kw.get(key, self.defaults[key]))
         self.auto_reload = self.auto_reload.lower() == 'true'
 
         assert self.directory and os.path.exists(self.directory), "Must specify an existing directory"
 
         # request handlers
-        self.handlers = [ GenshiRenderer ]
+        self.handlers = [ GenshiRenderer, Index ]
 
         # template loader
         self.template_dirs = self.template_dirs.split()
         self.template_dirs.append(resource_filename(__name__, 'templates'))
         self.loader = TemplateLoader(self.template_dirs,
                                      auto_reload=self.auto_reload)
 
     def __call__(self, environ, start_response):
diff --git a/simplewiki/handlers.py b/simplewiki/handlers.py
--- a/simplewiki/handlers.py
+++ b/simplewiki/handlers.py
@@ -79,13 +79,40 @@ class GenshiRenderer(Handler):
             raise HandlerMatchException
         self.data = { 'request': request,
                       'link': self.link }
 
     def __call__(self):
         return getattr(self, self.request.method.title())()
 
     def Get(self):
-        # needs to have self.template set
         template = self.app.loader.load(self.template)
         return Response(content_type='text/html',
                         body=template.generate(**self.data).render('html'))
 
+
+class Index(Handler):
+
+    template = 'index.html'
+
+    def __init__(self, app, request):
+        Handler.__init__(self, app, request)
+        self.directory = os.path.join(app.directory, *request.environ['path'])
+        if not os.path.isdir(self.directory):
+            raise HandlerMatchException
+        path = request.environ['path']
+        files = []
+        files = os.listdir(self.directory)
+        self.data = { 'request': request,
+                      'link': self.link,
+                      'directory': '/' + '/'.join(path),
+                      'files': files }
+
+    def __call__(self):
+        return getattr(self, self.request.method.title())()
+
+    def Get(self):
+        if not self.request.path_info.endswith('/'):
+            self.redirect(self.request.path_info + '/')
+        template = self.app.loader.load(self.template)
+        return Response(content_type='text/html',
+                        body=template.generate(**self.data).render('html'))
+
diff --git a/simplewiki/templates/index.html b/simplewiki/templates/index.html
--- a/simplewiki/templates/index.html
+++ b/simplewiki/templates/index.html
@@ -1,22 +1,16 @@
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
 <html xmlns="http://www.w3.org/1999/xhtml"
       xmlns:py="http://genshi.edgewall.org/"
       xmlns:xi="http://www.w3.org/2001/XInclude">
 <head>
-<title>Hello world</title>
-<script src="${link('jquery.js')}"></script>
-<script type="text/javascript">
-$(document).ready(function(){
-$(".text-input").click(function(){
-$(this).replaceWith('<form method="post"><input type="text" name="name" value="${name}"/><input type="submit" value="Go!"/></form>');
-});
-});
-</script>
+<title>${directory}</title>
 </head>
 <body>
-<xi:include href="navigation.html" />
-Hello <span class="text-input">${name}</span>!
+<ul>
+  <li py:if="request.path_info != '/'"><a href="..">..</a></li>
+  <li py:for="f in files"><a href="${f}">${f}</a></li>
+</ul>
 </body>
 </html>
diff --git a/simplewiki/templates/navigation.html b/simplewiki/templates/navigation.html
deleted file mode 100644
--- a/simplewiki/templates/navigation.html
+++ /dev/null
@@ -1,18 +0,0 @@
-<!DOCTYPE html
-    PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
-    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml"
-      xmlns:py="http://genshi.edgewall.org/"
-      xmlns:xi="http://www.w3.org/2001/XInclude"
-      py:strip="True">
-
-  <!-- nav bar -->
-  <div class="site-nav">
-    <ul>
-      <li py:for="link, name in links">
-        <a href="${link}">${name}</a>
-      </li>
-    </ul>
-  </div>
-    
-</html>

changes to add index handler

Step: add a POST handler

diff --git a/example/avatar.jpg b/example/avatar.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..94b272a84793e8c46e7b757406e6c1e87153119b
GIT binary patch
literal 3148
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

diff --git a/simplewiki/dispatcher.py b/simplewiki/dispatcher.py
--- a/simplewiki/dispatcher.py
+++ b/simplewiki/dispatcher.py
@@ -1,16 +1,16 @@
 """
 request dispatcher:
 data persisting across requests should go here
 """
 
 import os
 
-from handlers import GenshiRenderer, Index
+from handlers import GenshiRenderer, Index, Post
 
 from genshi.template import TemplateLoader
 from paste.fileapp import FileApp
 from pkg_resources import resource_filename
 from webob import Request, Response, exc
 
 class Dispatcher(object):
 
@@ -25,17 +25,17 @@ class Dispatcher(object):
         # set instance parameters from kw and defaults
         for key in self.defaults:
             setattr(self, key, kw.get(key, self.defaults[key]))
         self.auto_reload = self.auto_reload.lower() == 'true'
 
         assert self.directory and os.path.exists(self.directory), "Must specify an existing directory"
 
         # request handlers
-        self.handlers = [ GenshiRenderer, Index ]
+        self.handlers = [ Post, GenshiRenderer, Index ]
 
         # template loader
         self.template_dirs = self.template_dirs.split()
         self.template_dirs.append(resource_filename(__name__, 'templates'))
         self.loader = TemplateLoader(self.template_dirs,
                                      auto_reload=self.auto_reload)
 
     def __call__(self, environ, start_response):
diff --git a/simplewiki/handlers.py b/simplewiki/handlers.py
--- a/simplewiki/handlers.py
+++ b/simplewiki/handlers.py
@@ -111,8 +111,34 @@ class Index(Handler):
 
     def Get(self):
         if not self.request.path_info.endswith('/'):
             self.redirect(self.request.path_info + '/')
         template = self.app.loader.load(self.template)
         return Response(content_type='text/html',
                         body=template.generate(**self.data).render('html'))
 
+class Post(Handler):
+    methods = set(['POST']) # methods to listen to
+
+    def __init__(self, app, request):
+        Handler.__init__(self, app, request)
+        if 'file' not in request.POST:
+            raise HandlerMatchException
+        self.file = self.request.POST['file']
+        if not getattr(self.file, 'filename', None):
+            raise HandlerMatchException
+        self.location = request.path_info.rstrip('/')
+        path = os.path.join(self.app.directory, *self.request.environ['path'])
+        if os.path.isdir(path):
+            self.directory = path
+            self.filename = os.path.join(self.directory, self.file.filename)
+            self.location += '/' + self.file.filename
+        else:
+            self.directory = os.path.dirname(path)
+            self.filename = path
+        
+        f = file(self.filename, 'wb')
+        f.write(self.file.file.read())
+        f.close()
+        
+    def __call__(self):
+        self.redirect(self.location)
diff --git a/simplewiki/templates/index.html b/simplewiki/templates/index.html
--- a/simplewiki/templates/index.html
+++ b/simplewiki/templates/index.html
@@ -7,10 +7,16 @@
 <head>
 <title>${directory}</title>
 </head>
 <body>
 <ul>
   <li py:if="request.path_info != '/'"><a href="..">..</a></li>
   <li py:for="f in files"><a href="${f}">${f}</a></li>
 </ul>
+<form method="post" enctype="multipart/form-data">
+<p>Upload a file:</p>
+<input type="file" name="file"/>
+<input type="submit"/>
+</form>
+
 </body>
 </html>

changes to add POST handler

Step: add a file server

Add a FileApp handler for other content

diff --git a/simplewiki/dispatcher.py b/simplewiki/dispatcher.py
--- a/simplewiki/dispatcher.py
+++ b/simplewiki/dispatcher.py
@@ -1,16 +1,16 @@
 """
 request dispatcher:
 data persisting across requests should go here
 """
 
 import os
 
-from handlers import GenshiRenderer, Index, Post
+from handlers import GenshiRenderer, Index, Post, FileServer
 
 from genshi.template import TemplateLoader
 from paste.fileapp import FileApp
 from pkg_resources import resource_filename
 from webob import Request, Response, exc
 
 class Dispatcher(object):
 
@@ -25,17 +25,17 @@ class Dispatcher(object):
         # set instance parameters from kw and defaults
         for key in self.defaults:
             setattr(self, key, kw.get(key, self.defaults[key]))
         self.auto_reload = self.auto_reload.lower() == 'true'
 
         assert self.directory and os.path.exists(self.directory), "Must specify an existing directory"
 
         # request handlers
-        self.handlers = [ Post, GenshiRenderer, Index ]
+        self.handlers = [ Post, GenshiRenderer, Index, FileServer ]
 
         # template loader
         self.template_dirs = self.template_dirs.split()
         self.template_dirs.append(resource_filename(__name__, 'templates'))
         self.loader = TemplateLoader(self.template_dirs,
                                      auto_reload=self.auto_reload)
 
     def __call__(self, environ, start_response):
@@ -52,18 +52,11 @@ class Dispatcher(object):
         # match the request to a handler
         for h in self.handlers:
             handler = h.match(self, request)
             if handler is not None:
                 break
         else:
             handler = exc.HTTPNotFound
 
-        # add navigation links to handler [example]
-        if hasattr(handler, 'data'):
-            handler.data.setdefault('links', [])
-            for h in self.handlers:
-                handler.data['links'].append((handler.link(h.handler_path), 
-                                              h.__name__))
-
         # get response
         res = handler()
         return res(environ, start_response)
diff --git a/simplewiki/handlers.py b/simplewiki/handlers.py
--- a/simplewiki/handlers.py
+++ b/simplewiki/handlers.py
@@ -1,14 +1,15 @@
 """
 request handlers:
 these are instantiated for every request, then called
 """
 
 import os
+from paste.fileapp import FileApp 
 from urlparse import urlparse
 from webob import Response, exc
 
 class HandlerMatchException(Exception):
     """the handler doesn't match the request"""
 
 class Handler(object):
 
@@ -137,8 +138,21 @@ class Post(Handler):
             self.filename = path
         
         f = file(self.filename, 'wb')
         f.write(self.file.file.read())
         f.close()
         
     def __call__(self):
         self.redirect(self.location)
+
+class FileServer(Handler):
+    methods = set(['GET']) # methods to listen to
+
+    def __init__(self, app, request):
+        Handler.__init__(self, app, request)
+        self.file = os.path.join(self.app.directory, *request.environ['path'])
+        if not os.path.exists(self.file):
+            raise HandlerMatchException
+
+    def __call__(self):
+        return FileApp(self.file)
+        

changes to add file server

Step: add an edit view

diff --git a/example/hello.html b/example/hello.html
--- a/example/hello.html
+++ b/example/hello.html
@@ -1,8 +1,8 @@
-<html>
-<head>
-<title>Hello, world!</title>
-</head>
-<body>
-Hello, world!
-</body>
-</html>
+<html>
+<head>
+<title>Hello, world!</title>
+</head>
+<body>
+Hello, worldz!
+</body>
+</html>
diff --git a/simplewiki/dispatcher.py b/simplewiki/dispatcher.py
--- a/simplewiki/dispatcher.py
+++ b/simplewiki/dispatcher.py
@@ -1,16 +1,16 @@
 """
 request dispatcher:
 data persisting across requests should go here
 """
 
 import os
 
-from handlers import GenshiRenderer, Index, Post, FileServer
+from handlers import GenshiRenderer, Index, Post, FileServer, EditView
 
 from genshi.template import TemplateLoader
 from paste.fileapp import FileApp
 from pkg_resources import resource_filename
 from webob import Request, Response, exc
 
 class Dispatcher(object):
 
@@ -25,17 +25,17 @@ class Dispatcher(object):
         # set instance parameters from kw and defaults
         for key in self.defaults:
             setattr(self, key, kw.get(key, self.defaults[key]))
         self.auto_reload = self.auto_reload.lower() == 'true'
 
         assert self.directory and os.path.exists(self.directory), "Must specify an existing directory"
 
         # request handlers
-        self.handlers = [ Post, GenshiRenderer, Index, FileServer ]
+        self.handlers = [ Post, EditView, GenshiRenderer, Index, FileServer ]
 
         # template loader
         self.template_dirs = self.template_dirs.split()
         self.template_dirs.append(resource_filename(__name__, 'templates'))
         self.loader = TemplateLoader(self.template_dirs,
                                      auto_reload=self.auto_reload)
 
     def __call__(self, environ, start_response):
diff --git a/simplewiki/handlers.py b/simplewiki/handlers.py
--- a/simplewiki/handlers.py
+++ b/simplewiki/handlers.py
@@ -120,30 +120,34 @@ class Index(Handler):
 class Post(Handler):
     methods = set(['POST']) # methods to listen to
 
     def __init__(self, app, request):
         Handler.__init__(self, app, request)
         if 'file' not in request.POST:
             raise HandlerMatchException
         self.file = self.request.POST['file']
-        if not getattr(self.file, 'filename', None):
-            raise HandlerMatchException
+        filename = getattr(self.file, 'filename', '')
+        if filename:
+            content = self.file.file.read()
+        else:
+            content = self.file
+
         self.location = request.path_info.rstrip('/')
         path = os.path.join(self.app.directory, *self.request.environ['path'])
         if os.path.isdir(path):
             self.directory = path
-            self.filename = os.path.join(self.directory, self.file.filename)
-            self.location += '/' + self.file.filename
+            self.filename = os.path.join(self.directory, filename)
+            self.location += '/' + filename
         else:
             self.directory = os.path.dirname(path)
             self.filename = path
         
         f = file(self.filename, 'wb')
-        f.write(self.file.file.read())
+        f.write(content)
         f.close()
         
     def __call__(self):
         self.redirect(self.location)
 
 class FileServer(Handler):
     methods = set(['GET']) # methods to listen to
 
@@ -151,8 +155,34 @@ class FileServer(Handler):
         Handler.__init__(self, app, request)
         self.file = os.path.join(self.app.directory, *request.environ['path'])
         if not os.path.exists(self.file):
             raise HandlerMatchException
 
     def __call__(self):
         return FileApp(self.file)
         
+class EditView(Handler):
+    methods = set(['GET'])
+    template = 'edit.html'
+
+    def __init__(self, app, request):
+        if 'edit' not in request.GET:
+            raise HandlerMatchException
+        
+        Handler.__init__(self, app, request)
+
+        self.file = os.path.join(self.app.directory, *request.environ['path'])
+        if not os.path.exists(self.file):
+            raise HandlerMatchException
+        self.data = { 'request': request,
+                      'link': self.link,
+                      'file': '/' + '/'.join(request.environ['path']),
+                      'content': file(self.file).read()
+                      }
+
+    def __call__(self):
+        return getattr(self, self.request.method.title())()
+
+    def Get(self):
+        template = self.app.loader.load(self.template)
+        return Response(content_type='text/html',
+                        body=template.generate(**self.data).render('html'))

changes to add edit view

SimpleWiki: next steps

The code lives here: /hg/SimpleWiki

Defects:

Features:

The point is, its not hard to build an app this way. SimpleWiki is an example application an something to build on, not a comprehensive solution as-is.

See also cousin decoupage

Web services vs. web frameworks, revisited

In order to make a meaningful web service, it must be independent of other web services

Note that each of the handlers is a distinct web service. They don't talk to each other and have standalone functionality.

How you would do this in pylons:

How we could improve this in SimpleWiki: