Edgewall Software

Helper functions for use in Genshi templates

Often you need non-trivial presentation logic in templates, but dropping into Python code blocks for such tasks is not always appropriate. It's much more elegant to perform such tasks in the controller (i.e. the Python code feeding the template with data), or in helper functions that are called from within template expressions.

This page serves as a place where generalized functions that solve common tasks in presentation logic can be collected. At some point, Genshi might include a library of such functions.

Python Standard Library

Many of the Python builtin functions (such as reversed or sorted), as well as those in the itertools package (such as groupby), can be quite useful in templates. The builtin functions are available by default, whereas other functions need to be put in the template context data explicitly.

group()

The following was written by Christopher Lenz for use in the Trac project:

def group(iterable, num, predicate=None):
    """Combines the elements produced by the given iterable so that every `n`
    items are returned as a tuple.
    
    >>> items = [1, 2, 3, 4]
    >>> for item in group(items, 2):
    ...     print item
    (1, 2)
    (3, 4)
    
    The last tuple is padded with `None` values if its' length is smaller than
    `num`.
    
    >>> items = [1, 2, 3, 4, 5]
    >>> for item in group(items, 2):
    ...     print item
    (1, 2)
    (3, 4)
    (5, None)
    
    The optional `predicate` parameter can be used to flag elements that should
    not be packed together with other items. Only those elements where the
    predicate function returns True are grouped with other elements, otherwise
    they are returned as a tuple of length 1:
    
    >>> items = [1, 2, 3, 4]
    >>> for item in group(items, 2, lambda x: x != 3):
    ...     print item
    (1, 2)
    (3,)
    (4, None)
    """
    buf = []
    for item in iterable:
        flush = predicate and not predicate(item)
        if buf and flush:
            buf += [None] * (num - len(buf))
            yield tuple(buf)
            del buf[:]
        buf.append(item)
        if flush or len(buf) == num:
            yield tuple(buf)
            del buf[:]
    if buf:
        buf += [None] * (num - len(buf))
        yield tuple(buf)

If the predicate functionality is not needed, a vastly simpler implementation of that function would be:

def group(iterable, num):
    """Group an iterable into an n-tuples iterable. Incomplete tuples
    are discarded e.g.
    
    >>> list(group(range(10), 3))
    [(0, 1, 2), (3, 4, 5), (6, 7, 8), (9, None, None)]
    """
    return map(None, *[iter(iterable)] * num)

See also the Python Cookbook recipe “Group a list into sequential n-tuples”.

Example usage

<table py:with="fields = ['a', 'b', 'c', 'd', 'e']">
  <tr py:for="row in group(fields, 2)">
    <td py:for="cell in row">${cell}</td>
  </tr>
</table>

That should result in the following output:

<table>
  <tr>
    <td>a</td><td>b</td>
  </tr><tr>
    <td>c</td><td>d</td>
  </tr><tr>
    <td>e</td><td></td>
  </tr>
</table>

paginate()

A basic pagination routine written by Christopher Lenz.

from math import ceil

def paginate(items, page=0, max_per_page=10):
    """Simple generic pagination.
    
    Given an iterable, this function returns:
     * the slice of objects on the requested page,
     * the total number of items, and
     * the total number of pages.
    
    The `items` parameter can be a list, tuple, or iterator:
    
    >>> items = range(12)
    >>> items
    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
    >>> paginate(items)
    ([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 12, 2)
    >>> paginate(items, page=1)
    ([10, 11], 12, 2)
    >>> paginate(iter(items))
    ([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 12, 2)
    >>> paginate(iter(items), page=1)
    ([10, 11], 12, 2)
    
    This function also works with generators:
    
    >>> def generate():
    ...     for idx in range(12):
    ...         yield idx
    >>> paginate(generate())
    ([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 12, 2)
    >>> paginate(generate(), page=1)
    ([10, 11], 12, 2)
    
    The `max_per_page` parameter can be used to set the number of items that
    should be displayed per page:
    
    >>> items = range(12)
    >>> paginate(items, page=0, max_per_page=6)
    ([0, 1, 2, 3, 4, 5], 12, 2)
    >>> paginate(items, page=1, max_per_page=6)
    ([6, 7, 8, 9, 10, 11], 12, 2)
    """
    if not page:
        page = 0
    start = page * max_per_page
    stop = start + max_per_page

    count = None
    if hasattr(items, '__len__'):
        count = len(items)

    try: # Try slicing first for better performance
        retval = items[start:stop]
    except TypeError: # Slicing not supported, so iterate through the whole list
        retval = []
        for idx, item in enumerate(items):
            if start <= idx < stop:
                retval.append(item)
            # If we already obtained the total number of items via `len()`,
            # we can break out of the loop as soon as we've got the last item
            # for the requested page
            if count is not None and idx >= stop:
                break
        if count is None:
            count = idx + 1

    return retval, count, int(ceil(float(count) / max_per_page))

Example usage

<div py:with="items, num_items, num_pages = paginate(range(1, 24), cur_page - 1)">
  <h1>Page ${cur_page} of ${num_pages}</h1>
  <ul><li py:for="item in items">${item}</li></ul>
  <hr />
  <py:for each="num in range(cur_page, num_pages + 1)">
    <a href="?page=$num">$num</a>
  </py:for>
</div>

That should result in the following output:

<div>
  <h1>Page 1 of 3</h1>
  <ul><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li></ul>
  <hr />
  <a href="?page=1">1</a>
  <a href="?page=2">2</a>
  <a href="?page=3">3</a>
</div>

countdistinct()

Written by Christopher Lenz with a small bugfix by Arnar.

from itertools import groupby

def countdistinct(iterable, groups=None, key=None):
    """Count things.
    
    >>> items = ['red', 'green', 'blue', 'blue']
    >>> countdistinct(items)
    {'blue': 2, 'green': 1, 'red': 1}

    You can ensure that specific groups are always included in the result, even
    if they don't occur in the input:

    >>> items = ['red', 'blue', 'blue']
    >>> countdistinct(items, groups=['red', 'green', 'blue'])
    {'blue': 2, 'green': 0, 'red': 1}
    
    The optional `key` argument can be used to provide a function that returns
    the comparison key for every item:
    
    >>> from operator import itemgetter
    >>> items = [dict(name='foo', category='buzz'),
    ...          dict(name='bar', category='buzz')]
    >>> print countdistinct(items, key=itemgetter('category'))
    {'buzz': 2}
    """
    if groups is None: groups = []
    d = dict([(g, 0) for g in groups])
    for g, l in groupby(iterable, key=key):
        d[g] = len(list(l)) + d.get(g, 0)
    return d

See also: Documentation, GenshiRecipes

Last modified 17 years ago Last modified on Aug 28, 2007, 7:06:17 PM