| 454 | |
| 455 | == Factoring out the Templating == |
| 456 | |
| 457 | By now, we already have some repetitive code when it comes to rendering templates: both the `Root.index()` and the `Root.submit()` methods look very similar in that regard: they load a specific template, call its `generate()` method passing it some data, and then call the `render()` method of the resulting stream. As we're going to be adding more controller methods, let's factor out those things into a library module. |
| 458 | |
| 459 | There's a special challenge here, though: we still want to be able to add the `HTMLFormFiller` or other stream filters to the template output stream, which needs to be done before that output stream is serialized. We'll use a combination of a decorator and a regular function to achieve that, which collaborate using the !CherryPy thread-local context. |
| 460 | |
| 461 | Create a directory called `lib` inside the `geddit` directory, and inside the `lib` directory create two files, named `__init__.py` and `template.py`, respectively. Leave the first one empty, and in the second one, insert the following code: |
| 462 | |
| 463 | {{{ |
| 464 | #!python |
| 465 | import os |
| 466 | |
| 467 | import cherrypy |
| 468 | from genshi.core import Stream |
| 469 | from genshi.output import encode, get_serializer |
| 470 | from genshi.template import TemplateLoader |
| 471 | |
| 472 | loader = TemplateLoader( |
| 473 | os.path.join(os.path.dirname(__file__), '..', 'templates'), |
| 474 | auto_reload=True |
| 475 | ) |
| 476 | |
| 477 | def output(filename, method=None, encoding='utf-8', **options): |
| 478 | """Decorator for exposed methods to specify what template the should use |
| 479 | for rendering, and which serialization method and options should be |
| 480 | applied. |
| 481 | """ |
| 482 | def decorate(func): |
| 483 | def wrapper(*args, **kwargs): |
| 484 | cherrypy.thread_data.template = loader.load(filename) |
| 485 | serializer = get_serializer(method, **options) |
| 486 | stream = func(*args, **kwargs) |
| 487 | if not isinstance(stream, Stream): |
| 488 | return stream |
| 489 | return encode(serializer(stream), method=serializer, |
| 490 | encoding=encoding) |
| 491 | return wrapper |
| 492 | return decorate |
| 493 | |
| 494 | def render(*args, **kwargs): |
| 495 | """Function to render the given data to the template specified via the |
| 496 | ``@output`` decorator. |
| 497 | """ |
| 498 | if args: |
| 499 | assert len(args) == 1, \ |
| 500 | 'Expected exactly one argument, but got %r' % (args,) |
| 501 | template = loader.load(args[0]) |
| 502 | else: |
| 503 | template = cherrypy.thread_data.template |
| 504 | return template.generate(**kwargs) |
| 505 | }}} |
| 506 | |
| 507 | In the `genshi/controller.py` file, you can now remove the `from genshi.template import TemplateLoader` line, and also the instantiation of the `TemplateLoader`, as that is now done in our new library module. Of course, you'll have to import that library module instead: |
| 508 | |
| 509 | {{{ |
| 510 | #!python |
| 511 | from geddit.lib import template |
| 512 | }}} |
| 513 | |
| 514 | Now, we can change the `Root` class to match the following: |
| 515 | |
| 516 | {{{ |
| 517 | #!python |
| 518 | class Root(object): |
| 519 | |
| 520 | def __init__(self, data): |
| 521 | self.data = data |
| 522 | |
| 523 | @cherrypy.expose |
| 524 | @template.output('index.html', method='html', doctype='html') |
| 525 | def index(self): |
| 526 | return template.render(submissions=self.data) |
| 527 | |
| 528 | @cherrypy.expose |
| 529 | @template.output('submit.html', method='html', doctype='html') |
| 530 | def submit(self, cancel=False, **data): |
| 531 | if cherrypy.request.method == 'POST': |
| 532 | if cancel: |
| 533 | raise cherrypy.HTTPRedirect('/') |
| 534 | form = SubmissionForm() |
| 535 | try: |
| 536 | data = form.to_python(data) |
| 537 | submission = Submission(**data) |
| 538 | self.data.append(submission) |
| 539 | raise cherrypy.HTTPRedirect('/') |
| 540 | except Invalid, e: |
| 541 | errors = e.unpack_errors() |
| 542 | else: |
| 543 | errors = {} |
| 544 | |
| 545 | return template.render(errors=errors) | HTMLFormFiller(data=data) |
| 546 | }}} |
| 547 | |
| 548 | There's still some duplication of code in the decorators: the need to specify the serialization method and the doctype over and over again is suboptimal, and should ideally be made configurable in a central location. |