from django import template register = template.Library() @register.filter('break') def break_(loop): '''Breaks from a loop. The 'break' filter is used within a loop and takes as input a loop variable, e.g. 'forloop' in case of a for loop. For example, to display the items from list ``items`` up to the first item that is equal to ``end``:: Breaking from nested loops is also supported by passing the appropriate loop variable, e.g. ``forloop.parentloop|break``. ''' raise StopLoopException(loop, False) @register.filter('continue') def continue_(loop): '''Continues a loop by jumping to its beginning. The 'continue' filter is used within a loop and takes as input a loop variable, e.g. 'forloop' in case of a for loop. It can also be used (and is mostly useful) for nested loops by passing the appropriate loop variable, e.g. ``forloop.parentloop|continue``. For example:: {% for key,values in mapping.iteritems %}
{% for value in values %} {{ key }}: {{ value }}
{% if value|divisibleby:3 %} {{ value }} is divisible by 3
{{ forloop.parentloop|continue }} {% endif %} {% endfor %} {{ key }}: No value divisible by 3
{% endfor %} ''' raise StopLoopException(loop, True) # monkeypatch NodeList to handle break/continue def render(self, context): return template.mark_safe(''.join(map(template.force_unicode, _render_nodelist_items(self,context)))) template.NodeList.render = render # monkeypatch ForNode to handle break/continue def render(self, context): try: values = self.sequence.resolve(context, True) except template.VariableDoesNotExist: values = [] if values is None: values = [] if not hasattr(values, '__len__'): values = list(values) len_values = len(values) if len_values < 1: return self.nodelist_empty.render(context) if self.is_reversed: values = reversed(values) unpack = len(self.loopvars) > 1 # push a forloop value onto the context loop = BoundedLoop('forloop', context, self.nodelist_loop, len_values) for value in values: if unpack: # if there are multiple loop variables, unpack the value into them context.update(dict(zip(self.loopvars, value))) else: context[self.loopvars[0]] = value status = loop.next() if unpack and status is loop.PASS: # The loop variables were pushed on to the context so pop them # off again. This is necessary because the tag lets the length # of loopvars differ to the length of each set of items and we # don't want to leave any vars from the previous loop on the # context. If status is not PASS, all the additional dicts, # including the one with the loop variables, have already been # popped off in loop.next() so we don't have to pop it here context.pop() if status is loop.BREAK: break return loop.render(close=True) template.defaulttags.ForNode.render = render class StopLoopException(Exception): def __init__(self, loop, continue_, nodelist=None): if not isinstance(loop, Loop): raise TypeError('Loop instance expected, %s given' % loop.__class__.__name__) super(StopLoopException, self).__init__(loop, continue_, nodelist) self.loop, self.continue_, self.nodelist = self.args class Loop(dict): '''Base class of loop variables passed in the context (e.g. 'forloop'). A loop instance holds and keeps up to date the attributes exposed in the context. This class exposes ``counter``, ``counter0``, ``first`` and ``parentloop``; its :class:`BoundedLoop` subclass adds ``revcounter``, ``revcounter0`` and ``last``. Additionally, a loop instance renders the items of the nodelist that comprise the loop and accumulates the rendered strings on every call to :meth:`next`. :meth:`next` also handles continuing or breaking from the loop and informs the caller accordingly. ''' PASS = object() BREAK = object() CONTINUE = object() def __init__(self, name, context, nodelist): self._name = name self._context = context self._nodelist = nodelist self._rendered_nodelist = template.NodeList() self['parentloop'] = context.get(name) context.push() context[name] = self def render(self, close=False): '''Renders the accumulated nodelist for this loop. As a convenience, if ``close`` is true, the loop is also :meth:`close`d. ''' if close: self.close() return self._rendered_nodelist.render(self._context) render.alters_data = True def next(self): '''Updates this loop for one iteration step. :returns: The status of the loop after this step: :attr:`CONTINUE` if a ``continue`` targeting this loop was encountered, :attr:`BREAK` for a break, or :attr:`PASS` otherwise. :raises StopLoopException: If a ``break`` or ``continue`` for a loop other than this one (presumably an ancestor) was encountered. ''' if self._nodelist is None: raise RuntimeError('This loop is inactive') try: # update the exposed attributes counter = self['counter'] self.update(counter0=counter, counter=counter+1, first=False) except KeyError: # initialize the exposed attributes the first time this is called self.update(counter0=0, counter=1, first=True) try: _render_nodelist_items(self._nodelist, self._context, self._rendered_nodelist) status = self.PASS except StopLoopException, ex: # if this is not the target loop, keep bubbling up the exception if ex.loop is not self: raise # pop context until (but excluding) the dict that contains this loop self._pop_context_until_self(inclusive=False) status = ex.continue_ and self.CONTINUE or self.BREAK return status next.alters_data = True def close(self): '''Mark this loop as closed. After a loop is closed, subsequent calls to :meth:`next` are not allowed. This should be called when the loop is "done" to remove any loop-specific context entries. ''' if self._nodelist: self._pop_context_until_self(inclusive=True) self._nodelist = None close.alters_data = True def _pop_context_until_self(self, inclusive): name = self._name dicts = self._context.dicts while len(dicts) > 1: if dicts[-1].get(name) is self: if inclusive: del dicts[-1] break del dicts[-1] class BoundedLoop(Loop): '''A :class:`Loop` of known length. ``BoundedLoop`` instances expose ``revcounter``, ``revcounter0`` and ``last``, in addition to the attributes exposed by ``Loop`` itself. ''' def __init__(self, name, context, nodelist, length): if length < 1: raise ValueError('Length must be at least 1') self._length = length super(BoundedLoop, self).__init__(name, context, nodelist) def next(self): try: # update the exposed attributes revcounter0 = self['revcounter0'] if revcounter0 <= 0: raise RuntimeError('Attempted to call `next()` more than %d times' % self._length) self.update(revcounter0=revcounter0-1, revcounter=revcounter0, last=revcounter0==1) except KeyError: # initialize the exposed attributes the first time this is called length = self._length self.update(revcounter0=length-1, revcounter=length, last=length==1) return super(BoundedLoop, self).next() next.alters_data = True def _render_nodelist_items(nodelist, context, result=None): if result is None: result = [] for node in nodelist: if not isinstance(node, template.Node): result.append(node) else: try: result.append(nodelist.render_node(node, context)) except Exception, ex: # get the wrapped exception if settings.DEBUG is True if hasattr(ex, 'exc_info'): ex = ex.exc_info[1] # let every exception other than StopLoopException propagate if not isinstance(ex, StopLoopException): raise # reraise the StopLoopException with the updated nodelist if ex.nodelist: result.extend(ex.nodelist) ex.nodelist = result raise ex return result