root/trunk/diva/templating.py

Revision 203, 8.9 KB (checked in by cmlenz, 2 years ago)

Fixes for MIME type determination, and test improvements.

  • Property svn:eol-style set to native
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2007-2008 Christopher Lenz
4# All rights reserved.
5#
6# This software is licensed as described in the file COPYING, which
7# you should have received as part of this distribution.
8
9"""Helpers for templating and output serialization using the Genshi template
10engine.
11"""
12
13from datetime import date, datetime, time, timedelta
14from itertools import groupby
15import logging
16from operator import attrgetter, itemgetter
17
18from diva import app
19from diva.core import register_filter
20from diva.http import abort
21from diva.i18n import gettext, ngettext
22from diva.routing import abs_url, path_to
23from diva.util import decorator, relpath, truncate
24from genshi import Stream
25from genshi.input import HTML, XML
26from genshi.output import encode, get_serializer
27from genshi.template import Context, TemplateNotFound
28
29__all__ = ['output', 'render']
30__docformat__ = 'restructuredtext en'
31
32log = logging.getLogger('diva.templating')
33
34
35def output(templatename=None, method=None, mimetype=None, encoding=None,
36           **options):
37    """Decorator to specify template and output serialization.
38   
39    :param templatename: the name of the template file
40    :param method: the serialization method (one of "xml", "xhtml", "html", or
41                   "text")
42    :param mimetype: the content-type of the generated output
43    :param encoding: the encoding (character set) of the generated output
44    """
45    def decorate(func):
46
47        @decorator(func)
48        def wrapper(*args, **kwargs):
49            __traceback_hide__ = True
50            method_, mimetype_, encoding_ = method, mimetype, encoding
51            options_ = options.copy()
52
53            if templatename:
54                app.ctxt.template = app.templates.load(templatename)
55            else:
56                app.ctxt.template = None
57
58            response = app.ctxt.response
59            response.content_type = 'text/x-diva-temp'
60
61            stream = func(*args, **kwargs)
62            is_stream = isinstance(stream, Stream)
63
64            if is_stream and mimetype_ is None:
65                mimetype_, encoding_ = app.mime_types.guess_type(
66                    app.ctxt.template.filename
67                )
68                if encoding is not None:
69                    encoding_ = encoding
70            if mimetype_ is None:
71                mimetype_ = 'text/plain'
72            if encoding_ is None:
73                encoding_ = 'utf-8'
74            if is_stream and method_ is None:
75                method_ = {
76                    'text/html': 'html',
77                    'application/xhtml+xml': 'xhtml',
78                    'text/plain': 'plain'
79                }.get(mimetype_, 'xml')
80
81            if response.content_type == 'text/x-diva-temp':
82                response.content_type = mimetype_
83            response.charset = encoding_
84            if not is_stream:
85                return stream
86
87            if 'doctype' not in options_ and not app.ctxt.request.is_xhr \
88                    and mimetype_ in ('text/html', 'application/xhtml+xml'):
89                options_['doctype'] = app.config.get('doctype', 'html')
90            app.ctxt.serializer = get_serializer(method_, **options_)
91
92            return stream
93
94        return wrapper
95    return decorate
96
97
98# Helper functions for use in templates
99
100
101def classes(*args, **kwargs):
102    """Helper function for dynamically assembling a list of CSS class names
103    in templates.
104   
105    Any positional arguments are added to the list of class names. All
106    positional arguments must be strings:
107   
108    >>> classes('foo', 'bar')
109    u'foo bar'
110   
111    In addition, the names of any supplied keyword arguments are added if they
112    have a truth value:
113   
114    >>> classes('foo', bar=True)
115    u'foo bar'
116    >>> classes('foo', bar=False)
117    u'foo'
118   
119    If none of the arguments are added to the list, this function returns
120    `None`:
121   
122    >>> classes(bar=False)
123    """
124    classes = list(filter(None, args)) + [k for k, v in kwargs.items() if v]
125    if not classes:
126        return None
127    return u' '.join(classes)
128
129
130def group(iterable, num, predicate=None):
131    """Combines the elements produced by the given iterable so that every `num`
132    items are returned as a tuple.
133   
134    >>> items = [1, 2, 3, 4]
135    >>> for item in group(items, 2):
136    ...     print item
137    (1, 2)
138    (3, 4)
139   
140    The last tuple is padded with ``None`` values if its' length is smaller
141    than `num`.
142   
143    >>> items = [1, 2, 3, 4, 5]
144    >>> for item in group(items, 2):
145    ...     print item
146    (1, 2)
147    (3, 4)
148    (5, None)
149   
150    The optional `predicate` parameter can be used to flag elements that should
151    not be packed together with other items. Only those elements where the
152    predicate function returns True are grouped with other elements, otherwise
153    they are returned as a tuple of length 1:
154   
155    >>> items = [1, 2, 3, 4]
156    >>> for item in group(items, 2, lambda x: x != 3):
157    ...     print item
158    (1, 2)
159    (3,)
160    (4, None)
161   
162    :param iterable: the iterable to group
163    :param num: the number of items to pack into each tuple
164    :param predicate: an optional function called with the current item on
165                      every iteration; if the function returns ``False`` that
166                      item is emitted as a singleton tuple
167    """
168    buf = []
169    for item in iterable:
170        flush = predicate and not predicate(item)
171        if buf and flush:
172            buf += [None] * (num - len(buf))
173            yield tuple(buf)
174            del buf[:]
175        buf.append(item)
176        if flush or len(buf) == num:
177            yield tuple(buf)
178            del buf[:]
179    if buf:
180        buf += [None] * (num - len(buf))
181        yield tuple(buf)
182
183
184def separated(items, separator=', '):
185    """Separates a sequence of items by the specified string.
186   
187    >>> list(separated(['Peter', 'Paul', 'Mary']))
188    [('Peter', ', '), ('Paul', ', '), ('Mary', None)]
189
190    >>> list(separated([]))
191    []
192   
193    :param items: the iterable to separate
194    :param separator: the string (or other object) to use to separate the items
195    """
196    items = iter(items)
197    last = items.next()
198    for item in items:
199        yield last, separator
200        last = item
201    yield last, None
202
203
204defaults = {
205    'app': app,
206
207    # helper functions
208    'abs_url': abs_url,
209    'classes': classes,
210    'path_to': path_to,
211    'separated': separated,
212    'truncate': truncate,
213
214    # translation
215    '_': gettext,
216    'gettext': gettext,
217    'ngettext': ngettext,
218
219    # functional tools
220    'attrgetter': attrgetter,
221    'group': group,
222    'groupby': groupby,
223    'itemgetter': itemgetter,
224
225    # datetime
226    'date': date,
227    'datetime': datetime,
228    'time': time,
229    'timedelta': timedelta,
230
231    'HTML': HTML,
232    'XML': XML
233}
234
235
236def render(*args, **kwargs):
237    """Render the data given as keyword arguments using the selected template.
238   
239    The template can be specified either by using the `output` decorator, or
240    by passing the name of the template file as first argument to this function.
241   
242    Any keyword arguments are passed on into the template context. They will be
243    available for use in templates, in addition to the various default
244    variables (such as the ``request`` and ``response`` objects).
245    """
246    if args:
247        assert len(args) == 1, 'Expected one arg, but got %r' % (args,)
248        app.ctxt.template = app.templates.load(args[0])
249    log.debug('Render with variables %r', kwargs.keys())
250
251    ctxt = getattr(app.ctxt, 'context', None)
252    if ctxt is None:
253        ctxt = prepare_context(request=app.ctxt.request,
254                               response=app.ctxt.response)
255    ctxt.push(kwargs)
256
257    template = app.ctxt.template
258    assert template, 'No template specified'
259    log.debug('Render using template %r', relpath(template.filepath))
260    return template.generate(ctxt)
261
262
263def prepare_context(**kwargs):
264    data = defaults.copy() 
265    data.update(kwargs) 
266    ctxt = Context(**data)
267    app.ctxt.context = ctxt
268    return ctxt
269
270
271def serialize(stream):
272    if not isinstance(stream, Stream):
273        return stream
274    stream = Stream(list(stream))
275    charset = app.ctxt.response.charset
276    serializer = app.ctxt.serializer
277    log.debug('Serialize using %r with encoding %s', serializer, charset)
278    output = encode(serializer(stream), method=serializer, encoding=charset)
279    return output
280
281
282@register_filter('templating', requires='error-handling')
283def template_filter(request, response, chain):
284    """Request filter that serializes a Genshi template output stream."""
285    prepare_context(request=request, response=response)
286    return serialize(chain.next()(request, response, chain))
287
288
289@output()
290def view(request, response, template):
291    """Generic request handler that renders a template with only the default
292    data.
293   
294    :param request: the request object
295    :param response: the response object
296    :param template: the file name of the template
297    """
298    try:
299        return render(template)
300    except TemplateNotFound:
301        abort(404)
Note: See TracBrowser for help on using the browser.