thirdparty/google_appengine/lib/webob/docs/wiki-example.txt
changeset 109 620f9b141567
equal deleted inserted replaced
108:261778de26ff 109:620f9b141567
       
     1 Wiki Example
       
     2 ============
       
     3 
       
     4 :author: Ian Bicking <ianb@colorstudy.com>
       
     5 
       
     6 .. contents::
       
     7 
       
     8 Introduction
       
     9 ------------
       
    10 
       
    11 This is an example of how to write a WSGI application using WebOb.
       
    12 WebOb isn't itself intended to write applications -- it is not a web
       
    13 framework on its own -- but it is *possible* to write applications
       
    14 using just WebOb.
       
    15 
       
    16 The `file serving example <file-example.html>`_ is a better example of
       
    17 advanced HTTP usage.  The `comment middleware example
       
    18 <comment-example.html>`_ is a better example of using middleware.
       
    19 This example provides some completeness by showing an
       
    20 application-focused end point.
       
    21 
       
    22 This example implements a very simple wiki.
       
    23 
       
    24 Code
       
    25 ----
       
    26 
       
    27 The finished code for this is available in
       
    28 `docs/wiki-example-code/example.py
       
    29 <http://svn.pythonpaste.org/Paste/WebOb/trunk/docs/wiki-example-code/example.py>`_
       
    30 -- you can run that file as a script to try it out.
       
    31 
       
    32 Creating an Application
       
    33 -----------------------
       
    34 
       
    35 A common pattern for creating small WSGI applications is to have a
       
    36 class which is instantiated with the configuration.  For our
       
    37 application we'll be storing the pages under a directory.
       
    38 
       
    39 .. code-block::
       
    40 
       
    41     class WikiApp(object):
       
    42 
       
    43         def __init__(self, storage_dir):
       
    44             self.storage_dir = os.path.abspath(os.path.normpath(storage_dir))
       
    45 
       
    46 WSGI applications are callables like ``wsgi_app(environ,
       
    47 start_response)``.  *Instances* of `WikiApp` are WSGI applications, so
       
    48 we'll implement a ``__call__`` method:
       
    49 
       
    50 .. code-block::
       
    51 
       
    52     class WikiApp(object):
       
    53         ...
       
    54         def __call__(self, environ, start_response):
       
    55             # what we'll fill in
       
    56 
       
    57 To make the script runnable we'll create a simple command-line
       
    58 interface:
       
    59 
       
    60 .. code-block::
       
    61 
       
    62     if __name__ == '__main__':
       
    63         import optparse
       
    64         parser = optparse.OptionParser(
       
    65             usage='%prog --port=PORT'
       
    66             )
       
    67         parser.add_option(
       
    68             '-p', '--port',
       
    69             default='8080',
       
    70             dest='port',
       
    71             type='int',
       
    72             help='Port to serve on (default 8080)')
       
    73         parser.add_option(
       
    74             '--wiki-data',
       
    75             default='./wiki',
       
    76             dest='wiki_data',
       
    77             help='Place to put wiki data into (default ./wiki/)')
       
    78         options, args = parser.parse_args()
       
    79         print 'Writing wiki pages to %s' % options.wiki_data
       
    80         app = WikiApp(options.wiki_data)
       
    81         from wsgiref.simple_server import make_server
       
    82         httpd = make_server('localhost', options.port, app)
       
    83         print 'Serving on http://localhost:%s' % options.port
       
    84         try:
       
    85             httpd.serve_forever()
       
    86         except KeyboardInterrupt:
       
    87             print '^C'
       
    88 
       
    89 There's not much to talk about in this code block.  The application is
       
    90 instantiated and served with the built-in module
       
    91 `wsgiref.simple_server
       
    92 <http://www.python.org/doc/current/lib/module-wsgiref.simple_server.html>`_.
       
    93 
       
    94 The WSGI Application
       
    95 --------------------
       
    96 
       
    97 Of course all the interesting stuff is in that ``__call__`` method.
       
    98 WebOb lets you ignore some of the details of WSGI, like what
       
    99 ``start_response`` really is.  ``environ`` is a CGI-like dictionary,
       
   100 but ``webob.Request`` gives an object interface to it.
       
   101 ``webob.Response`` represents a response, and is itself a WSGI
       
   102 application.  Here's kind of the hello world of WSGI applications
       
   103 using these objects:
       
   104 
       
   105 .. code-block::
       
   106 
       
   107     from webob import Request, Response
       
   108 
       
   109     class WikiApp(object):
       
   110         ...
       
   111 
       
   112         def __call__(self, environ, start_response):
       
   113             req = Request(environ)
       
   114             resp = Response(
       
   115                 'Hello %s!' % req.params.get('name', 'World'))
       
   116             return resp(environ, start_response)
       
   117 
       
   118 ``req.params.get('name', 'World')`` gets any query string parameter
       
   119 (like ``?name=Bob``), or if it's a POST form request it will look for
       
   120 a form parameter ``name``.  We instantiate the response with the body
       
   121 of the response.  You could also give keyword arguments like
       
   122 ``content_type='text/plain'`` (``text/html`` is the default content
       
   123 type and ``200 OK`` is the default status).
       
   124 
       
   125 For the wiki application we'll support a couple different kinds of
       
   126 screens, and we'll make our ``__call__`` method dispatch to different
       
   127 methods depending on the request.  We'll support an ``action``
       
   128 parameter like ``?action=edit``, and also dispatch on the method (GET,
       
   129 POST, etc, in ``req.method``).  We'll pass in the request and expect a
       
   130 response object back.
       
   131 
       
   132 Also, WebOb has a series of exceptions in ``webob.exc``, like
       
   133 ``webob.exc.HTTPNotFound``, ``webob.exc.HTTPTemporaryRedirect``, etc.
       
   134 We'll also let the method raise one of these exceptions and turn it
       
   135 into a response.
       
   136 
       
   137 One last thing we'll do in our ``__call__`` method is create our
       
   138 ``Page`` object, which represents a wiki page.
       
   139 
       
   140 All this together makes:
       
   141 
       
   142 .. code-block::
       
   143 
       
   144     from webob import Request, Response
       
   145     from webob import exc
       
   146 
       
   147     class WikiApp(object):
       
   148         ...
       
   149 
       
   150         def __call__(self, environ, start_response):
       
   151             req = Request(environ)
       
   152             action = req.params.get('action', 'view')
       
   153             # Here's where we get the Page domain object:
       
   154             page = self.get_page(req.path_info)
       
   155             try:
       
   156                 try:
       
   157                     # The method name is action_{action_param}_{request_method}:
       
   158                     meth = getattr(self, 'action_%s_%s' % (action, req.method))
       
   159                 except AttributeError:
       
   160                     # If the method wasn't found there must be
       
   161                     # something wrong with the request:
       
   162                     raise exc.HTTPBadRequest('No such action %r' % action).exception
       
   163                 resp = meth(req, page)
       
   164             except exc.HTTPException, e:
       
   165                 # The exception object itself is a WSGI application/response:
       
   166                 resp = e
       
   167             return resp(environ, start_response)
       
   168 
       
   169 The Domain Object
       
   170 -----------------
       
   171 
       
   172 The ``Page`` domain object isn't really related to the web, but it is
       
   173 important to implementing this.  Each ``Page`` is just a file on the
       
   174 filesystem.  Our ``get_page`` method figures out the filename given
       
   175 the path (the path is in ``req.path_info``, which is all the path
       
   176 after the base path).  The ``Page`` class handles getting and setting
       
   177 the title and content.
       
   178 
       
   179 Here's the method to figure out the filename:
       
   180 
       
   181 .. code-block::
       
   182 
       
   183     import os
       
   184 
       
   185     class WikiApp(object):
       
   186         ...
       
   187 
       
   188         def get_page(self, path):
       
   189             path = path.lstrip('/')
       
   190             if not path:
       
   191                 # The path was '/', the home page
       
   192                 path = 'index'
       
   193             path = os.path.join(self.storage_dir)
       
   194             path = os.path.normpath(path)
       
   195             if path.endswith('/'):
       
   196                 path += 'index'
       
   197             if not path.startswith(self.storage_dir):
       
   198                 raise exc.HTTPBadRequest("Bad path").exception
       
   199             path += '.html'
       
   200             return Page(path)
       
   201 
       
   202 Mostly this is just the kind of careful path construction you have to
       
   203 do when mapping a URL to a filename.  While the server *may* normalize
       
   204 the path (so that a path like ``/../../`` can't be requested), you can
       
   205 never really be sure.  By using ``os.path.normpath`` we eliminate
       
   206 these, and then we make absolutely sure that the resulting path is
       
   207 under our ``self.storage_dir`` with ``if not
       
   208 path.startswith(self.storage_dir): raise exc.HTTPBadRequest("Bad
       
   209 path").exception``.
       
   210 
       
   211 .. note::
       
   212 
       
   213     ``exc.HTTPBadRequest("Bad path")`` is a ``webob.Response`` object.
       
   214     This is a new-style class, so you can't raise it in Python 2.4 or
       
   215     under (only old-style classes work).  The attribute ``.exception``
       
   216     can actually be raised.  The exception object is *also* a WSGI
       
   217     application, though it doesn't have attributes like
       
   218     ``.content_type``, etc.
       
   219 
       
   220 Here's the actual domain object:
       
   221 
       
   222 .. code-block::
       
   223 
       
   224     class Page(object):
       
   225         def __init__(self, filename):
       
   226             self.filename = filename
       
   227 
       
   228         @property
       
   229         def exists(self):
       
   230             return os.path.exists(self.filename)
       
   231 
       
   232         @property
       
   233         def title(self):
       
   234             if not self.exists:
       
   235                 # we need to guess the title
       
   236                 basename = os.path.splitext(os.path.basename(self.filename))[0]
       
   237                 basename = re.sub(r'[_-]', ' ', basename)
       
   238                 return basename.capitalize()
       
   239             content = self.full_content
       
   240             match = re.search(r'<title>(.*?)</title>', content, re.I|re.S)
       
   241             return match.group(1)
       
   242     
       
   243         @property
       
   244         def full_content(self):
       
   245             f = open(self.filename, 'rb')
       
   246             try:
       
   247                 return f.read()
       
   248             finally:
       
   249                 f.close()
       
   250     
       
   251         @property
       
   252         def content(self):
       
   253             if not self.exists:
       
   254                 return ''
       
   255             content = self.full_content
       
   256             match = re.search(r'<body[^>]*>(.*?)</body>', content, re.I|re.S)
       
   257             return match.group(1)
       
   258 
       
   259         @property
       
   260         def mtime(self):
       
   261             if not self.exists:
       
   262                 return None
       
   263             else:
       
   264                 return os.stat(self.filename).st_mtime
       
   265 
       
   266         def set(self, title, content):
       
   267             dir = os.path.dirname(self.filename)
       
   268             if not os.path.exists(dir):
       
   269                 os.makedirs(dir)
       
   270             new_content = """<html><head><title>%s</title></head><body>%s</body></html>""" % (
       
   271                 title, content)
       
   272             f = open(self.filename, 'wb')
       
   273             f.write(new_content)
       
   274             f.close()
       
   275             
       
   276 Basically it provides a ``.title`` attribute, a ``.content``
       
   277 attribute, the ``.mtime`` (last modified time), and the page can exist
       
   278 or not (giving appropriate guesses for title and content when the page
       
   279 does not exist).  It encodes these on the filesystem as a simple HTML
       
   280 page that is parsed by some regular expressions.
       
   281 
       
   282 None of this really applies much to the web or WebOb, so I'll leave it
       
   283 to you to figure out the details of this.
       
   284 
       
   285 URLs, PATH_INFO, and SCRIPT_NAME
       
   286 --------------------------------
       
   287 
       
   288 This is an aside for the tutorial, but an important concept.  In WSGI,
       
   289 and accordingly with WebOb, the URL is split up into several pieces.
       
   290 Some of these are obvious and some not.
       
   291 
       
   292 An example::
       
   293 
       
   294   http://example.com:8080/wiki/article/12?version=10
       
   295 
       
   296 There are several components here:
       
   297 
       
   298 * req.scheme: ``http``
       
   299 * req.host: ``example.com:8080``
       
   300 * req.server_name: ``example.com``
       
   301 * req.server_port: 8080
       
   302 * req.script_name: ``/wiki``
       
   303 * req.path_info: ``/article/12``
       
   304 * req.query_string: ``version=10``
       
   305 
       
   306 One non-obvious part is ``req.script_name`` and ``req.path_info``.
       
   307 These correspond to the CGI environmental variables ``SCRIPT_NAME``
       
   308 and ``PATH_INFO``.  ``req.script_name`` points to the *application*.
       
   309 You might have several applications in your site at different paths:
       
   310 one at ``/wiki``, one at ``/blog``, one at ``/``.  Each application
       
   311 doesn't necessarily know about the others, but it has to construct its
       
   312 URLs properly -- so any internal links to the wiki application should
       
   313 start with ``/wiki``.
       
   314 
       
   315 Just as there are pieces to the URL, there are several properties in
       
   316 WebOb to construct URLs based on these:
       
   317 
       
   318 * req.host_url: ``http://example.com:8080``
       
   319 * req.application_url: ``http://example.com:8080/wiki``
       
   320 * req.path_url: ``http://example.com:8080/wiki/article/12``
       
   321 * req.path: ``/wiki/article/12``
       
   322 * req.path_qs: ``/wiki/article/12?version=10``
       
   323 * req.url: ``http://example.com:8080/wiki/article/12?version10``
       
   324 
       
   325 You can also create URLs with
       
   326 ``req.relative_url('some/other/page')``.  In this example that would
       
   327 resolve to ``http://example.com:8080/wiki/article/some/other/page``.
       
   328 You can also create a relative URL to the application URL
       
   329 (SCRIPT_NAME) like ``req.relative_url('some/other/page', True)`` which
       
   330 would be ``http://example.com:8080/wiki/some/other/page``.
       
   331 
       
   332 Back to the Application
       
   333 -----------------------
       
   334 
       
   335 We have a dispatching function with ``__call__`` and we have a domain
       
   336 object with ``Page``, but we aren't actually doing anything.
       
   337 
       
   338 The dispatching goes to ``action_ACTION_METHOD``, where ACTION
       
   339 defaults to ``view``.  So a simple page view will be
       
   340 ``action_view_GET``.  Let's implement that:
       
   341 
       
   342 .. code-block::
       
   343 
       
   344     class WikiApp(object):
       
   345         ...
       
   346 
       
   347         def action_view_GET(self, req, page):
       
   348             if not page.exists:
       
   349                 return exc.HTTPTemporaryRedirect(
       
   350                     location=req.url + '?action=edit')
       
   351             text = self.view_template.substitute(
       
   352                 page=page, req=req)
       
   353             resp = Response(text)
       
   354             resp.last_modified = page.mtime
       
   355             resp.conditional_response = True
       
   356             return resp
       
   357 
       
   358 The first thing we do is redirect the user to the edit screen if the
       
   359 page doesn't exist.  ``exc.HTTPTemporaryRedirect`` is a response that
       
   360 gives a ``307 Temporary Redirect`` response with the given location.
       
   361 
       
   362 Otherwise we fill in a template.  The template language we're going to
       
   363 use in this example is `Tempita <http://pythonpaste.org/tempita/>`_, a
       
   364 very simple template language with a similar interface to
       
   365 `string.Template <>`_.
       
   366 
       
   367 The template actually looks like this:
       
   368 
       
   369 .. code-block::
       
   370 
       
   371     from tempita import HTMLTemplate
       
   372 
       
   373     VIEW_TEMPLATE = HTMLTemplate("""\
       
   374     <html>
       
   375      <head>
       
   376       <title>{{page.title}}</title>
       
   377      </head>
       
   378      <body>
       
   379     <h1>{{page.title}}</h1>
       
   380 
       
   381     <div>{{page.content|html}}</div>
       
   382 
       
   383     <hr>
       
   384     <a href="{{req.url}}?action=edit">Edit</a>
       
   385      </body>
       
   386     </html>
       
   387     """)
       
   388 
       
   389     class WikiApp(object):
       
   390         view_template = VIEW_TEMPLATE
       
   391         ...
       
   392 
       
   393 As you can see it's a simple template using the title and the body,
       
   394 and a link to the edit screen.  We copy the template object into a
       
   395 class method (``view_template = VIEW_TEMPLATE``) so that potentially a
       
   396 subclass could override these templates.
       
   397 
       
   398 ``tempita.HTMLTemplate`` is a template that does automatic HTML
       
   399 escaping.  Our wiki will just be written in plain HTML, so we disable
       
   400 escaping of the content with ``{{page.content|html}}``.
       
   401 
       
   402 So let's look at the ``action_view_GET`` method again:
       
   403 
       
   404 .. code-block::
       
   405 
       
   406         def action_view_GET(self, req, page):
       
   407             if not page.exists:
       
   408                 return exc.HTTPTemporaryRedirect(
       
   409                     location=req.url + '?action=edit')
       
   410             text = self.view_template.substitute(
       
   411                 page=page, req=req)
       
   412             resp = Response(text)
       
   413             resp.last_modified = page.mtime
       
   414             resp.conditional_response = True
       
   415             return resp
       
   416 
       
   417 The template should be pretty obvious now.  We create a response with
       
   418 ``Response(text)``, which already has a default Content-Type of
       
   419 ``text/html``.
       
   420 
       
   421 To allow conditional responses we set ``resp.last_modified``.  You can
       
   422 set this attribute to a date, None (effectively removing the header),
       
   423 a time tuple (like produced by ``time.localtime()``), or as in this
       
   424 case to an integer timestamp.  If you get the value back it will
       
   425 always be a `datetime <>`_ object (or None).  With this header we can
       
   426 process requests with If-Modified-Since headers, and return ``304 Not
       
   427 Modified`` if appropriate.  It won't actually do that unless you set
       
   428 ``resp.conditional_response`` to True.  
       
   429 
       
   430 .. note::
       
   431 
       
   432     If you subclass ``webob.Response`` you can set the class attribute
       
   433     ``default_conditional_response = True`` and this setting will be
       
   434     on by default.  You can also set other defaults, like the
       
   435     ``default_charset`` (``"utf8"``), or ``default_content_type``
       
   436     (``"text/html"``).
       
   437 
       
   438 The Edit Screen
       
   439 ---------------
       
   440 
       
   441 The edit screen will be implemented in the method
       
   442 ``action_edit_GET``.  There's a template and a very simple method:
       
   443 
       
   444 .. code-block::
       
   445 
       
   446     EDIT_TEMPLATE = HTMLTemplate("""\
       
   447     <html>
       
   448      <head>
       
   449       <title>Edit: {{page.title}}</title>
       
   450      </head>
       
   451      <body>
       
   452     {{if page.exists}}
       
   453     <h1>Edit: {{page.title}}</h1>
       
   454     {{else}}
       
   455     <h1>Create: {{page.title}}</h1>
       
   456     {{endif}}
       
   457 
       
   458     <form action="{{req.path_url}}" method="POST">
       
   459      <input type="hidden" name="mtime" value="{{page.mtime}}">
       
   460      Title: <input type="text" name="title" style="width: 70%" value="{{page.title}}"><br>
       
   461      Content: <input type="submit" value="Save"> 
       
   462      <a href="{{req.path_url}}">Cancel</a>
       
   463        <br>
       
   464      <textarea name="content" style="width: 100%; height: 75%" rows="40">{{page.content}}</textarea>
       
   465        <br>
       
   466      <input type="submit" value="Save">
       
   467      <a href="{{req.path_url}}">Cancel</a>
       
   468     </form>
       
   469     </body></html>
       
   470     """)
       
   471 
       
   472     class WikiApp(object):
       
   473         ...
       
   474 
       
   475         edit_template = EDIT_TEMPLATE
       
   476 
       
   477         def action_edit_GET(self, req, page):
       
   478             text = self.edit_template.substitute(
       
   479                 page=page, req=req)
       
   480             return Response(text)
       
   481 
       
   482 As you can see, all the action here is in the template.  
       
   483 
       
   484 In ``<form action="{{req.path_url}}" method="POST">`` we submit to
       
   485 ``req.path_url``; that's everything *but* ``?action=edit``.  So we are
       
   486 POSTing right over the view page.  This has the nice side effect of
       
   487 automatically invalidating any caches of the original page.  It also
       
   488 is vaguely `RESTful <>`_.
       
   489 
       
   490 We save the last modified time in a hidden ``mtime`` field.  This way
       
   491 we can detect concurrent updates.  If start editing the page who's
       
   492 mtime is 100000, and someone else edits and saves a revision changing
       
   493 the mtime to 100010, we can use this hidden field to detect that
       
   494 conflict.  Actually resolving the conflict is a little tricky and
       
   495 outside the scope of this particular tutorial, we'll just note the
       
   496 conflict to the user in an error.
       
   497 
       
   498 From there we just have a very straight-forward HTML form.  Note that
       
   499 we don't quote the values because that is done automatically by
       
   500 ``HTMLTemplate``; if you are using something like ``string.Template``
       
   501 or a templating language that doesn't do automatic quoting, you have
       
   502 to be careful to quote all the field values.
       
   503 
       
   504 We don't have any error conditions in our application, but if there
       
   505 were error conditions we might have to re-display this form with the
       
   506 input values the user already gave.  In that case we'd do something
       
   507 like::
       
   508 
       
   509     <input type="text" name="title"
       
   510      value="{{req.params.get('title', page.title)}}">
       
   511 
       
   512 This way we use the value in the request (``req.params`` is both the
       
   513 query string parameters and any variables in a POST response), but if
       
   514 there is no value (e.g., first request) then we use the page values.
       
   515 
       
   516 Processing the Form
       
   517 -------------------
       
   518 
       
   519 The form submits to ``action_view_POST`` (``view`` is the default
       
   520 action).  So we have to implement that method:
       
   521 
       
   522 .. code-block::
       
   523 
       
   524     class WikiApp(object):
       
   525         ...
       
   526 
       
   527         def action_view_POST(self, req, page):
       
   528             submit_mtime = int(req.params.get('mtime') or '0') or None
       
   529             if page.mtime != submit_mtime:
       
   530                 return exc.HTTPPreconditionFailed(
       
   531                     "The page has been updated since you started editing it")
       
   532             page.set(
       
   533                 title=req.params['title'],
       
   534                 content=req.params['content'])
       
   535             resp = exc.HTTPSeeOther(
       
   536                 location=req.path_url)
       
   537             return resp
       
   538 
       
   539 The first thing we do is check the mtime value.  It can be an empty
       
   540 string (when there's no mtime, like when you are creating a page) or
       
   541 an integer.  ``int(req.params.get('time') or '0') or None`` basically
       
   542 makes sure we don't pass ``""`` to ``int()`` (which is an error) then
       
   543 turns 0 into None (``0 or None`` will evaluate to None in Python --
       
   544 ``false_value or other_value`` in Python resolves to ``other_value``).
       
   545 If it fails we just give a not-very-helpful error message, using ``412
       
   546 Precondition Failed`` (typically preconditions are HTTP headers like
       
   547 ``If-Unmodified-Since``, but we can't really get the browser to send
       
   548 requests like that, so we use the hidden field instead).
       
   549 
       
   550 .. note::
       
   551 
       
   552     Error statuses in HTTP are often under-used because people think
       
   553     they need to either return an error (useful for machines) or an
       
   554     error message or interface (useful for humans).  In fact you can
       
   555     do both: you can give any human readable error message with your
       
   556     error response.
       
   557 
       
   558     One problem is that Internet Explorer will replace error messages
       
   559     with its own incredibly unhelpful error messages.  However, it
       
   560     will only do this if the error message is short.  If it's fairly
       
   561     large (4Kb is large enough) it will show the error message it was
       
   562     given.  You can load your error with a big HTML comment to
       
   563     accomplish this, like ``"<!-- %s -->" % ('x'*4000)``.
       
   564 
       
   565     You can change the status of any response with ``resp.status_int =
       
   566     412``, or you can change the body of an ``exc.HTTPSomething`` with
       
   567     ``resp.body = new_body``.  The primary advantage of using the
       
   568     classes in ``webob.exc`` is giving the response a clear name and a
       
   569     boilerplate error message.
       
   570 
       
   571 After we check the mtime we get the form parameters from
       
   572 ``req.params`` and issue a redirect back to the original view page.
       
   573 ``303 See Other`` is a good response to give after accepting a POST
       
   574 form submission, as it gets rid of the POST (no warning messages for the
       
   575 user if they try to go back).
       
   576 
       
   577 In this example we've used ``req.params`` for all the form values.  If
       
   578 we wanted to be specific about where we get the values from, they
       
   579 could come from ``req.GET`` (the query string, a misnomer since the
       
   580 query string is present even in POST requests) or ``req.POST`` (a POST
       
   581 form body).  While sometimes it's nice to distinguish between these
       
   582 two locations, for the most part it doesn't matter.  If you want to
       
   583 check the request method (e.g., make sure you can't change a page with
       
   584 a GET request) there's no reason to do it by accessing these
       
   585 method-specific getters.  It's better to just handle the method
       
   586 specifically.  We do it here by including the request method in our
       
   587 dispatcher (dispatching to ``action_view_GET`` or
       
   588 ``action_view_POST``).
       
   589 
       
   590 
       
   591 Cookies
       
   592 -------
       
   593 
       
   594 One last little improvement we can do is show the user a message when
       
   595 they update the page, so it's not quite so mysteriously just another
       
   596 page view.
       
   597 
       
   598 A simple way to do this is to set a cookie after the save, then
       
   599 display it in the page view.  To set it on save, we add a little to
       
   600 ``action_view_POST``:
       
   601 
       
   602 .. code-block::
       
   603 
       
   604     def action_view_POST(self, req, page):
       
   605         ...
       
   606         resp = exc.HTTPSeeOther(
       
   607             location=req.path_url)
       
   608         resp.set_cookie('message', 'Page updated')
       
   609         return resp
       
   610 
       
   611 And then in ``action_view_GET``:
       
   612 
       
   613 .. code-block::
       
   614 
       
   615 
       
   616     VIEW_TEMPLATE = HTMLTemplate("""\
       
   617     ...
       
   618     {{if message}}
       
   619     <div style="background-color: #99f">{{message}}</div>
       
   620     {{endif}}
       
   621     ...""")
       
   622 
       
   623     class WikiApp(object):
       
   624         ...
       
   625 
       
   626         def action_view_GET(self, req, page):
       
   627             ...
       
   628             if req.cookies.get('message'):
       
   629                 message = req.cookies['message']
       
   630             else:
       
   631                 message = None
       
   632             text = self.view_template.substitute(
       
   633                 page=page, req=req, message=message)
       
   634             resp = Response(text)
       
   635             if message:
       
   636                 resp.delete_cookie('message')
       
   637             else:
       
   638                 resp.last_modified = page.mtime
       
   639                 resp.conditional_response = True
       
   640             return resp
       
   641 
       
   642 ``req.cookies`` is just a dictionary, and we also delete the cookie if
       
   643 it is present (so the message doesn't keep getting set).  The
       
   644 conditional response stuff only applies when there isn't any 
       
   645 message, as messages are private.  Another alternative would be to
       
   646 display the message with Javascript, like::
       
   647 
       
   648     <script type="text/javascript">
       
   649     function readCookie(name) {
       
   650         var nameEQ = name + "=";
       
   651         var ca = document.cookie.split(';');
       
   652         for (var i=0; i < ca.length; i++) {
       
   653             var c = ca[i];
       
   654             while (c.charAt(0) == ' ') c = c.substring(1,c.length);
       
   655             if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
       
   656         }
       
   657         return null;
       
   658     }
       
   659     
       
   660     function createCookie(name, value, days) {
       
   661         if (days) {
       
   662             var date = new Date();
       
   663             date.setTime(date.getTime()+(days*24*60*60*1000));
       
   664             var expires = "; expires="+date.toGMTString();
       
   665         } else {
       
   666             var expires = "";
       
   667         }
       
   668         document.cookie = name+"="+value+expires+"; path=/";
       
   669     }
       
   670 
       
   671     function eraseCookie(name) {
       
   672         createCookie(name, "", -1);
       
   673     }
       
   674 
       
   675     function showMessage() {
       
   676         var message = readCookie('message');
       
   677         if (message) {
       
   678             var el = document.getElementById('message');
       
   679             el.innerHTML = message;
       
   680             el.style.display = '';
       
   681             eraseCookie('message');
       
   682         }
       
   683     }
       
   684     </script>
       
   685 
       
   686 Then put ``<div id="messaage" style="display: none"></div>`` in the
       
   687 page somewhere.  This has the advantage of being very cacheable and
       
   688 simple on the server side.
       
   689 
       
   690 Conclusion
       
   691 ----------
       
   692 
       
   693 We're done, hurrah!