| 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 |
|---|
| 10 | engine. |
|---|
| 11 | """ |
|---|
| 12 | |
|---|
| 13 | from datetime import date, datetime, time, timedelta |
|---|
| 14 | from itertools import groupby |
|---|
| 15 | import logging |
|---|
| 16 | from operator import attrgetter, itemgetter |
|---|
| 17 | |
|---|
| 18 | from diva import app |
|---|
| 19 | from diva.core import register_filter |
|---|
| 20 | from diva.http import abort |
|---|
| 21 | from diva.i18n import gettext, ngettext |
|---|
| 22 | from diva.routing import abs_url, path_to |
|---|
| 23 | from diva.util import decorator, relpath, truncate |
|---|
| 24 | from genshi import Stream |
|---|
| 25 | from genshi.input import HTML, XML |
|---|
| 26 | from genshi.output import encode, get_serializer |
|---|
| 27 | from genshi.template import Context, TemplateNotFound |
|---|
| 28 | |
|---|
| 29 | __all__ = ['output', 'render'] |
|---|
| 30 | __docformat__ = 'restructuredtext en' |
|---|
| 31 | |
|---|
| 32 | log = logging.getLogger('diva.templating') |
|---|
| 33 | |
|---|
| 34 | |
|---|
| 35 | def 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 | |
|---|
| 101 | def 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 | |
|---|
| 130 | def 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 | |
|---|
| 184 | def 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 | |
|---|
| 204 | defaults = { |
|---|
| 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 | |
|---|
| 236 | def 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 | |
|---|
| 263 | def 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 | |
|---|
| 271 | def 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') |
|---|
| 283 | def 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() |
|---|
| 290 | def 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) |
|---|