|
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! |