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