diff options
| author | Christopher Long <indirecthit@gmail.com> | 2007-06-17 22:18:54 +0000 |
|---|---|---|
| committer | Christopher Long <indirecthit@gmail.com> | 2007-06-17 22:18:54 +0000 |
| commit | ae22b6d403dcf25098c77f0dfcf59ae58b186461 (patch) | |
| tree | c37fc631e99a7e4d909d6b6d236f495003731ea7 /django/template | |
| parent | 0cf7bc439129c66df8d64601e885f83b256b4f25 (diff) | |
per-object-permissions: Merged to trunk [5486] NOTE: Not fully tested, will be working on this over the next few weeks.
git-svn-id: http://code.djangoproject.com/svn/django/branches/per-object-permissions@5488 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Diffstat (limited to 'django/template')
| -rw-r--r-- | django/template/__init__.py | 189 | ||||
| -rw-r--r-- | django/template/context.py | 9 | ||||
| -rw-r--r-- | django/template/defaultfilters.py | 135 | ||||
| -rw-r--r-- | django/template/defaulttags.py | 347 | ||||
| -rw-r--r-- | django/template/loader.py | 33 | ||||
| -rw-r--r-- | django/template/loader_tags.py | 27 |
6 files changed, 531 insertions, 209 deletions
diff --git a/django/template/__init__.py b/django/template/__init__.py index 7718801684..7495eea878 100644 --- a/django/template/__init__.py +++ b/django/template/__init__.py @@ -55,6 +55,7 @@ times with multiple contexts) '\n<html>\n\n</html>\n' """ import re +import types from inspect import getargspec from django.conf import settings from django.template.context import Context, RequestContext, ContextPopException @@ -91,12 +92,18 @@ UNKNOWN_SOURCE="<unknown source>" tag_re = re.compile('(%s.*?%s|%s.*?%s|%s.*?%s)' % (re.escape(BLOCK_TAG_START), re.escape(BLOCK_TAG_END), re.escape(VARIABLE_TAG_START), re.escape(VARIABLE_TAG_END), re.escape(COMMENT_TAG_START), re.escape(COMMENT_TAG_END))) +# matches if the string is valid number +number_re = re.compile(r'[-+]?(\d+|\d*\.\d+)$') # global dictionary of libraries that have been loaded using get_library libraries = {} # global list of libraries to load by default for a new parser builtins = [] +# True if TEMPLATE_STRING_IF_INVALID contains a format string (%s). None means +# uninitialised. +invalid_var_format_string = None + class TemplateSyntaxError(Exception): def __str__(self): try: @@ -117,7 +124,13 @@ class TemplateDoesNotExist(Exception): pass class VariableDoesNotExist(Exception): - pass + + def __init__(self, msg, params=()): + self.msg = msg + self.params = params + + def __str__(self): + return self.msg % self.params class InvalidTemplateLibrary(Exception): pass @@ -155,9 +168,12 @@ class Template(object): for subnode in node: yield subnode - def render(self, context): + def iter_render(self, context): "Display stage -- can be called many times" - return self.nodelist.render(context) + return self.nodelist.iter_render(context) + + def render(self, context): + return ''.join(self.iter_render(context)) def compile_string(template_string, origin): "Compiles template_string into NodeList ready for rendering" @@ -185,18 +201,27 @@ class Lexer(object): def tokenize(self): "Return a list of tokens from a given template_string" - # remove all empty strings, because the regex has a tendency to add them - bits = filter(None, tag_re.split(self.template_string)) - return map(self.create_token, bits) + in_tag = False + result = [] + for bit in tag_re.split(self.template_string): + if bit: + result.append(self.create_token(bit, in_tag)) + in_tag = not in_tag + return result - def create_token(self,token_string): - "Convert the given token string into a new Token object and return it" - if token_string.startswith(VARIABLE_TAG_START): - token = Token(TOKEN_VAR, token_string[len(VARIABLE_TAG_START):-len(VARIABLE_TAG_END)].strip()) - elif token_string.startswith(BLOCK_TAG_START): - token = Token(TOKEN_BLOCK, token_string[len(BLOCK_TAG_START):-len(BLOCK_TAG_END)].strip()) - elif token_string.startswith(COMMENT_TAG_START): - token = Token(TOKEN_COMMENT, '') + def create_token(self, token_string, in_tag): + """ + Convert the given token string into a new Token object and return it. + If in_tag is True, we are processing something that matched a tag, + otherwise it should be treated as a literal string. + """ + if in_tag: + if token_string.startswith(VARIABLE_TAG_START): + token = Token(TOKEN_VAR, token_string[len(VARIABLE_TAG_START):-len(VARIABLE_TAG_END)].strip()) + elif token_string.startswith(BLOCK_TAG_START): + token = Token(TOKEN_BLOCK, token_string[len(BLOCK_TAG_START):-len(BLOCK_TAG_END)].strip()) + elif token_string.startswith(COMMENT_TAG_START): + token = Token(TOKEN_COMMENT, '') else: token = Token(TOKEN_TEXT, token_string) return token @@ -207,22 +232,22 @@ class DebugLexer(Lexer): def tokenize(self): "Return a list of tokens from a given template_string" - token_tups, upto = [], 0 + result, upto = [], 0 for match in tag_re.finditer(self.template_string): start, end = match.span() if start > upto: - token_tups.append( (self.template_string[upto:start], (upto, start)) ) + result.append(self.create_token(self.template_string[upto:start], (upto, start), False)) upto = start - token_tups.append( (self.template_string[start:end], (start,end)) ) + result.append(self.create_token(self.template_string[start:end], (start, end), True)) upto = end last_bit = self.template_string[upto:] if last_bit: - token_tups.append( (last_bit, (upto, upto + len(last_bit))) ) - return [self.create_token(tok, (self.origin, loc)) for tok, loc in token_tups] + result.append(self.create_token(last_bit, (upto, upto + len(last_bit)), False)) + return result - def create_token(self, token_string, source): - token = super(DebugLexer, self).create_token(token_string) - token.source = source + def create_token(self, token_string, source, in_tag): + token = super(DebugLexer, self).create_token(token_string, in_tag) + token.source = self.origin, source return token class Parser(object): @@ -330,7 +355,7 @@ class Parser(object): return FilterExpression(token, self) def find_filter(self, filter_name): - if self.filters.has_key(filter_name): + if filter_name in self.filters: return self.filters[filter_name] else: raise TemplateSyntaxError, "Invalid filter: '%s'" % filter_name @@ -458,7 +483,7 @@ class TokenParser(object): while i < len(subject) and subject[i] != c: i += 1 if i >= len(subject): - raise TemplateSyntaxError, "Searching for value. Unexpected end of string in column %d: %s" % subject + raise TemplateSyntaxError, "Searching for value. Unexpected end of string in column %d: %s" % (i, subject) i += 1 s = subject[p:i] while i < len(subject) and subject[i] in (' ', '\t'): @@ -467,9 +492,6 @@ class TokenParser(object): self.pointer = i return s - - - filter_raw_string = r""" ^%(i18n_open)s"(?P<i18n_constant>%(str)s)"%(i18n_close)s| ^"(?P<constant>%(str)s)"| @@ -547,7 +569,7 @@ class FilterExpression(object): filters.append( (filter_func,args)) upto = match.end() if upto != len(token): - raise TemplateSyntaxError, "Could not parse the remainder: %s" % token[upto:] + raise TemplateSyntaxError, "Could not parse the remainder: '%s' from '%s'" % (token[upto:], token) self.var, self.filters = var, filters def resolve(self, context, ignore_failures=False): @@ -558,6 +580,11 @@ class FilterExpression(object): obj = None else: if settings.TEMPLATE_STRING_IF_INVALID: + global invalid_var_format_string + if invalid_var_format_string is None: + invalid_var_format_string = '%s' in settings.TEMPLATE_STRING_IF_INVALID + if invalid_var_format_string: + return settings.TEMPLATE_STRING_IF_INVALID % self.var return settings.TEMPLATE_STRING_IF_INVALID else: obj = settings.TEMPLATE_STRING_IF_INVALID @@ -574,6 +601,8 @@ class FilterExpression(object): def args_check(name, func, provided): provided = list(provided) plen = len(provided) + # Check to see if a decorator is providing the real function. + func = getattr(func, '_decorated_function', func) args, varargs, varkw, defaults = getargspec(func) # First argument is filter input. args.pop(0) @@ -624,12 +653,9 @@ def resolve_variable(path, context): (The example assumes VARIABLE_ATTRIBUTE_SEPARATOR is '.') """ - if path[0].isdigit(): + if number_re.match(path): number_type = '.' in path and float or int - try: - current = number_type(path) - except ValueError: - current = settings.TEMPLATE_STRING_IF_INVALID + current = number_type(path) elif path[0] in ('"', "'") and path[0] == path[-1]: current = path[1:-1] else: @@ -659,8 +685,12 @@ def resolve_variable(path, context): except (TypeError, AttributeError): try: # list-index lookup current = current[int(bits[0])] - except (IndexError, ValueError, KeyError): - raise VariableDoesNotExist, "Failed lookup for key [%s] in %r" % (bits[0], current) # missing attribute + except (IndexError, # list index out of range + ValueError, # invalid literal for int() + KeyError, # current is a dict without `int(bits[0])` key + TypeError, # unsubscriptable object + ): + raise VariableDoesNotExist("Failed lookup for key [%s] in %r", (bits[0], current)) # missing attribute except Exception, e: if getattr(e, 'silent_variable_failure', False): current = settings.TEMPLATE_STRING_IF_INVALID @@ -669,10 +699,26 @@ def resolve_variable(path, context): del bits[0] return current +class NodeBase(type): + def __new__(cls, name, bases, attrs): + """ + Ensures that either a 'render' or 'render_iter' method is defined on + any Node sub-class. This avoids potential infinite loops at runtime. + """ + if not (isinstance(attrs.get('render'), types.FunctionType) or + isinstance(attrs.get('iter_render'), types.FunctionType)): + raise TypeError('Unable to create Node subclass without either "render" or "iter_render" method.') + return type.__new__(cls, name, bases, attrs) + class Node(object): + __metaclass__ = NodeBase + + def iter_render(self, context): + return (self.render(context),) + def render(self, context): "Return the node rendered as a string" - pass + return ''.join(self.iter_render(context)) def __iter__(self): yield self @@ -688,13 +734,12 @@ class Node(object): class NodeList(list): def render(self, context): - bits = [] + return ''.join(self.iter_render(context)) + + def iter_render(self, context): for node in self: - if isinstance(node, Node): - bits.append(self.render_node(node, context)) - else: - bits.append(node) - return ''.join(bits) + for chunk in node.iter_render(context): + yield chunk def get_nodes_by_type(self, nodetype): "Return a list of all nodes of the given type" @@ -703,24 +748,25 @@ class NodeList(list): nodes.extend(node.get_nodes_by_type(nodetype)) return nodes - def render_node(self, node, context): - return(node.render(context)) - class DebugNodeList(NodeList): - def render_node(self, node, context): - try: - result = node.render(context) - except TemplateSyntaxError, e: - if not hasattr(e, 'source'): - e.source = node.source - raise - except Exception, e: - from sys import exc_info - wrapped = TemplateSyntaxError('Caught an exception while rendering: %s' % e) - wrapped.source = node.source - wrapped.exc_info = exc_info() - raise wrapped - return result + def iter_render(self, context): + for node in self: + if not isinstance(node, Node): + yield node + continue + try: + for chunk in node.iter_render(context): + yield chunk + except TemplateSyntaxError, e: + if not hasattr(e, 'source'): + e.source = node.source + raise + except Exception, e: + from sys import exc_info + wrapped = TemplateSyntaxError('Caught an exception while rendering: %s' % e) + wrapped.source = node.source + wrapped.exc_info = exc_info() + raise wrapped class TextNode(Node): def __init__(self, s): @@ -729,6 +775,9 @@ class TextNode(Node): def __repr__(self): return "<Text Node: '%s'>" % self.s[:25] + def iter_render(self, context): + return (self.s,) + def render(self, context): return self.s @@ -752,6 +801,9 @@ class VariableNode(Node): else: return output + def iter_render(self, context): + return (self.render(context),) + def render(self, context): output = self.filter_expression.resolve(context) return self.encode_output(output) @@ -806,7 +858,7 @@ class Library(object): raise InvalidTemplateLibrary, "Unsupported arguments to Library.tag: (%r, %r)", (name, compile_function) def tag_function(self,func): - self.tags[func.__name__] = func + self.tags[getattr(func, "_decorated_function", func).__name__] = func return func def filter(self, name=None, filter_func=None): @@ -830,7 +882,7 @@ class Library(object): raise InvalidTemplateLibrary, "Unsupported arguments to Library.filter: (%r, %r)", (name, filter_func) def filter_function(self, func): - self.filters[func.__name__] = func + self.filters[getattr(func, "_decorated_function", func).__name__] = func return func def simple_tag(self,func): @@ -840,13 +892,16 @@ class Library(object): def __init__(self, vars_to_resolve): self.vars_to_resolve = vars_to_resolve + #def iter_render(self, context): + # return (self.render(context),) + def render(self, context): resolved_vars = [resolve_variable(var, context) for var in self.vars_to_resolve] return func(*resolved_vars) - compile_func = curry(generic_tag_compiler, params, defaults, func.__name__, SimpleNode) + compile_func = curry(generic_tag_compiler, params, defaults, getattr(func, "_decorated_function", func).__name__, SimpleNode) compile_func.__doc__ = func.__doc__ - self.tag(func.__name__, compile_func) + self.tag(getattr(func, "_decorated_function", func).__name__, compile_func) return func def inclusion_tag(self, file_name, context_class=Context, takes_context=False): @@ -862,7 +917,7 @@ class Library(object): def __init__(self, vars_to_resolve): self.vars_to_resolve = vars_to_resolve - def render(self, context): + def iter_render(self, context): resolved_vars = [resolve_variable(var, context) for var in self.vars_to_resolve] if takes_context: args = [context] + resolved_vars @@ -878,11 +933,11 @@ class Library(object): else: t = get_template(file_name) self.nodelist = t.nodelist - return self.nodelist.render(context_class(dict)) + return self.nodelist.iter_render(context_class(dict)) - compile_func = curry(generic_tag_compiler, params, defaults, func.__name__, InclusionNode) + compile_func = curry(generic_tag_compiler, params, defaults, getattr(func, "_decorated_function", func).__name__, InclusionNode) compile_func.__doc__ = func.__doc__ - self.tag(func.__name__, compile_func) + self.tag(getattr(func, "_decorated_function", func).__name__, compile_func) return func return dec diff --git a/django/template/context.py b/django/template/context.py index ba23e95ab7..59650b05fe 100644 --- a/django/template/context.py +++ b/django/template/context.py @@ -35,7 +35,7 @@ class Context(object): def __getitem__(self, key): "Get a variable's value, starting at the current context and going upward" for d in self.dicts: - if d.has_key(key): + if key in d: return d[key] raise KeyError(key) @@ -45,13 +45,16 @@ class Context(object): def has_key(self, key): for d in self.dicts: - if d.has_key(key): + if key in d: return True return False + def __contains__(self, key): + return self.has_key(key) + def get(self, key, otherwise=None): for d in self.dicts: - if d.has_key(key): + if key in d: return d[key] return otherwise diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py index 969ef7b28b..bbaceba24a 100644 --- a/django/template/defaultfilters.py +++ b/django/template/defaultfilters.py @@ -2,12 +2,44 @@ from django.template import resolve_variable, Library from django.conf import settings -from django.utils.translation import gettext +from django.utils.translation import gettext, ngettext import re import random as random_module register = Library() +####################### +# STRING DECORATOR # +####################### + +def smart_string(obj): + # FUTURE: Unicode strings should probably be normalized to a specific + # encoding and non-unicode strings should be converted to unicode too. +# if isinstance(obj, unicode): +# obj = obj.encode(settings.DEFAULT_CHARSET) +# else: +# obj = unicode(obj, settings.DEFAULT_CHARSET) + # FUTURE: Replace dumb string logic below with cool unicode logic above. + if not isinstance(obj, basestring): + obj = str(obj) + return obj + +def stringfilter(func): + """ + Decorator for filters which should only receive strings. The object passed + as the first positional argument will be converted to a string. + """ + def _dec(*args, **kwargs): + if args: + args = list(args) + args[0] = smart_string(args[0]) + return func(*args, **kwargs) + + # Include a reference to the real function (used to check original + # arguments by the template parser). + _dec._decorated_function = getattr(func, '_decorated_function', func) + return _dec + ################### # STRINGS # ################### @@ -16,31 +48,52 @@ register = Library() def addslashes(value): "Adds slashes - useful for passing strings to JavaScript, for example." return value.replace('\\', '\\\\').replace('"', '\\"').replace("'", "\\'") +addslashes = stringfilter(addslashes) def capfirst(value): "Capitalizes the first character of the value" - value = str(value) return value and value[0].upper() + value[1:] - +capfirst = stringfilter(capfirst) + def fix_ampersands(value): "Replaces ampersands with ``&`` entities" from django.utils.html import fix_ampersands return fix_ampersands(value) +fix_ampersands = stringfilter(fix_ampersands) -def floatformat(text): +def floatformat(text, arg=-1): """ - Displays a floating point number as 34.2 (with one decimal place) -- but - only if there's a point to be displayed + If called without an argument, displays a floating point + number as 34.2 -- but only if there's a point to be displayed. + With a positive numeric argument, it displays that many decimal places + always. + With a negative numeric argument, it will display that many decimal + places -- but only if there's places to be displayed. + Examples: + + * num1 = 34.23234 + * num2 = 34.00000 + * num1|floatformat results in 34.2 + * num2|floatformat is 34 + * num1|floatformat:3 is 34.232 + * num2|floatformat:3 is 34.000 + * num1|floatformat:-3 is 34.232 + * num2|floatformat:-3 is 34 """ try: f = float(text) except ValueError: return '' + try: + d = int(arg) + except ValueError: + return smart_string(f) m = f - int(f) - if m: - return '%.1f' % f - else: + if not m and d < 0: return '%d' % int(f) + else: + formatstr = '%%.%df' % abs(d) + return formatstr % f def linenumbers(value): "Displays text with line numbers" @@ -51,22 +104,26 @@ def linenumbers(value): for i, line in enumerate(lines): lines[i] = ("%0" + width + "d. %s") % (i + 1, escape(line)) return '\n'.join(lines) +linenumbers = stringfilter(linenumbers) def lower(value): "Converts a string into all lowercase" return value.lower() +lower = stringfilter(lower) def make_list(value): """ Returns the value turned into a list. For an integer, it's a list of digits. For a string, it's a list of characters. """ - return list(str(value)) + return list(value) +make_list = stringfilter(make_list) def slugify(value): "Converts to lowercase, removes non-alpha chars and converts spaces to hyphens" value = re.sub('[^\w\s-]', '', value).strip().lower() return re.sub('[-\s]+', '-', value) +slugify = stringfilter(slugify) def stringformat(value, arg): """ @@ -78,13 +135,14 @@ def stringformat(value, arg): of Python string formatting """ try: - return ("%" + arg) % value + return ("%" + str(arg)) % value except (ValueError, TypeError): return "" def title(value): "Converts a string into titlecase" return re.sub("([a-z])'([A-Z])", lambda m: m.group(0).lower(), value.title()) +title = stringfilter(title) def truncatewords(value, arg): """ @@ -100,20 +158,42 @@ def truncatewords(value, arg): if not isinstance(value, basestring): value = str(value) return truncate_words(value, length) +truncatewords = stringfilter(truncatewords) + +def truncatewords_html(value, arg): + """ + Truncates HTML after a certain number of words + + Argument: Number of words to truncate after + """ + from django.utils.text import truncate_html_words + try: + length = int(arg) + except ValueError: # invalid literal for int() + return value # Fail silently. + if not isinstance(value, basestring): + value = str(value) + return truncate_html_words(value, length) +truncatewords_html = stringfilter(truncatewords_html) def upper(value): "Converts a string into all uppercase" return value.upper() +upper = stringfilter(upper) def urlencode(value): "Escapes a value for use in a URL" import urllib + if not isinstance(value, basestring): + value = str(value) return urllib.quote(value) +urlencode = stringfilter(urlencode) def urlize(value): "Converts URLs in plain text into clickable links" from django.utils.html import urlize return urlize(value, nofollow=True) +urlize = stringfilter(urlize) def urlizetrunc(value, limit): """ @@ -124,10 +204,12 @@ def urlizetrunc(value, limit): """ from django.utils.html import urlize return urlize(value, trim_url_limit=int(limit), nofollow=True) +urlizetrunc = stringfilter(urlizetrunc) def wordcount(value): "Returns the number of words" return len(value.split()) +wordcount = stringfilter(wordcount) def wordwrap(value, arg): """ @@ -136,7 +218,8 @@ def wordwrap(value, arg): Argument: number of characters to wrap the text at. """ from django.utils.text import wrap - return wrap(str(value), int(arg)) + return wrap(value, int(arg)) +wordwrap = stringfilter(wordwrap) def ljust(value, arg): """ @@ -144,7 +227,8 @@ def ljust(value, arg): Argument: field size """ - return str(value).ljust(int(arg)) + return value.ljust(int(arg)) +ljust = stringfilter(ljust) def rjust(value, arg): """ @@ -152,15 +236,18 @@ def rjust(value, arg): Argument: field size """ - return str(value).rjust(int(arg)) + return value.rjust(int(arg)) +rjust = stringfilter(rjust) def center(value, arg): "Centers the value in a field of a given width" - return str(value).center(int(arg)) + return value.center(int(arg)) +center = stringfilter(center) def cut(value, arg): "Removes all values of arg from the given string" return value.replace(arg, '') +cut = stringfilter(cut) ################### # HTML STRINGS # @@ -170,15 +257,18 @@ def escape(value): "Escapes a string's HTML" from django.utils.html import escape return escape(value) +escape = stringfilter(escape) def linebreaks(value): "Converts newlines into <p> and <br />s" from django.utils.html import linebreaks return linebreaks(value) +linebreaks = stringfilter(linebreaks) def linebreaksbr(value): "Converts newlines into <br />s" return value.replace('\n', '<br />') +linebreaksbr = stringfilter(linebreaksbr) def removetags(value, tags): "Removes a space separated list of [X]HTML tags from the output" @@ -189,13 +279,13 @@ def removetags(value, tags): value = starttag_re.sub('', value) value = endtag_re.sub('', value) return value +removetags = stringfilter(removetags) def striptags(value): "Strips all [X]HTML tags" from django.utils.html import strip_tags - if not isinstance(value, basestring): - value = str(value) return strip_tags(value) +striptags = stringfilter(striptags) ################### # LISTS # @@ -230,7 +320,7 @@ def first(value): def join(value, arg): "Joins a list with a string, like Python's ``str.join(list)``" try: - return arg.join(map(str, value)) + return arg.join(map(smart_string, value)) except AttributeError: # fail silently but nicely return value @@ -427,12 +517,12 @@ def filesizeformat(bytes): return "0 bytes" if bytes < 1024: - return "%d byte%s" % (bytes, bytes != 1 and 's' or '') + return ngettext("%(size)d byte", "%(size)d bytes", bytes) % {'size': bytes} if bytes < 1024 * 1024: - return "%.1f KB" % (bytes / 1024) + return gettext("%.1f KB") % (bytes / 1024) if bytes < 1024 * 1024 * 1024: - return "%.1f MB" % (bytes / (1024 * 1024)) - return "%.1f GB" % (bytes / (1024 * 1024 * 1024)) + return gettext("%.1f MB") % (bytes / (1024 * 1024)) + return gettext("%.1f GB") % (bytes / (1024 * 1024 * 1024)) def pluralize(value, arg='s'): """ @@ -516,6 +606,7 @@ register.filter(timesince) register.filter(timeuntil) register.filter(title) register.filter(truncatewords) +register.filter(truncatewords_html) register.filter(unordered_list) register.filter(upper) register.filter(urlencode) diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index 3f3f4bda56..77fac6bec5 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -4,13 +4,22 @@ from django.template import Node, NodeList, Template, Context, resolve_variable from django.template import TemplateSyntaxError, VariableDoesNotExist, BLOCK_TAG_START, BLOCK_TAG_END, VARIABLE_TAG_START, VARIABLE_TAG_END, SINGLE_BRACE_START, SINGLE_BRACE_END, COMMENT_TAG_START, COMMENT_TAG_END from django.template import get_library, Library, InvalidTemplateLibrary from django.conf import settings +from django.utils.itercompat import groupby import sys +import re + +if not hasattr(__builtins__, 'reversed'): + # For Python 2.3. + # From http://www.python.org/doc/current/tut/node11.html + def reversed(data): + for index in xrange(len(data)-1, -1, -1): + yield data[index] register = Library() class CommentNode(Node): - def render(self, context): - return '' + def iter_render(self, context): + return () class CycleNode(Node): def __init__(self, cyclevars, variable_name=None): @@ -19,6 +28,9 @@ class CycleNode(Node): self.counter = -1 self.variable_name = variable_name + def iter_render(self, context): + return (self.render(context),) + def render(self, context): self.counter += 1 value = self.cyclevars[self.counter % self.cyclevars_len] @@ -27,26 +39,32 @@ class CycleNode(Node): return value class DebugNode(Node): - def render(self, context): + def iter_render(self, context): from pprint import pformat - output = [pformat(val) for val in context] - output.append('\n\n') - output.append(pformat(sys.modules)) - return ''.join(output) + for val in context: + yield pformat(val) + yield "\n\n" + yield pformat(sys.modules) class FilterNode(Node): def __init__(self, filter_expr, nodelist): self.filter_expr, self.nodelist = filter_expr, nodelist - def render(self, context): + def iter_render(self, context): output = self.nodelist.render(context) # apply filters - return self.filter_expr.resolve(Context({'var': output})) + context.update({'var': output}) + filtered = self.filter_expr.resolve(context) + context.pop() + return (filtered,) class FirstOfNode(Node): def __init__(self, vars): self.vars = vars + def iter_render(self, context): + return (self.render(context),) + def render(self, context): for var in self.vars: try: @@ -58,8 +76,8 @@ class FirstOfNode(Node): return '' class ForNode(Node): - def __init__(self, loopvar, sequence, reversed, nodelist_loop): - self.loopvar, self.sequence = loopvar, sequence + def __init__(self, loopvars, sequence, reversed, nodelist_loop): + self.loopvars, self.sequence = loopvars, sequence self.reversed = reversed self.nodelist_loop = nodelist_loop @@ -69,7 +87,7 @@ class ForNode(Node): else: reversed = '' return "<For Node: for %s in %s, tail_len: %d%s>" % \ - (self.loopvar, self.sequence, len(self.nodelist_loop), reversed) + (', '.join( self.loopvars ), self.sequence, len(self.nodelist_loop), reversed) def __iter__(self): for node in self.nodelist_loop: @@ -82,28 +100,24 @@ class ForNode(Node): nodes.extend(self.nodelist_loop.get_nodes_by_type(nodetype)) return nodes - def render(self, context): - nodelist = NodeList() - if context.has_key('forloop'): + def iter_render(self, context): + if 'forloop' in context: parentloop = context['forloop'] else: parentloop = {} context.push() try: values = self.sequence.resolve(context, True) + if values is None: + values = () + elif not hasattr(values, '__len__'): + values = list(values) except VariableDoesNotExist: - values = [] - if values is None: - values = [] - if not hasattr(values, '__len__'): - values = list(values) + values = () len_values = len(values) if self.reversed: - # From http://www.python.org/doc/current/tut/node11.html - def reverse(data): - for index in range(len(data)-1, -1, -1): - yield data[index] - values = reverse(values) + values = reversed(values) + unpack = len(self.loopvars) > 1 for i, item in enumerate(values): context['forloop'] = { # shortcuts for current loop iteration number @@ -117,11 +131,26 @@ class ForNode(Node): 'last': (i == len_values - 1), 'parentloop': parentloop, } - context[self.loopvar] = item + if unpack: + # If there are multiple loop variables, unpack the item into + # them. + context.update(dict(zip(self.loopvars, item))) + else: + context[self.loopvars[0]] = item + + # We inline this to avoid the overhead since ForNode is pretty + # common. for node in self.nodelist_loop: - nodelist.append(node.render(context)) + for chunk in node.iter_render(context): + yield chunk + if unpack: + # 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. + context.pop() context.pop() - return nodelist.render(context) class IfChangedNode(Node): def __init__(self, nodelist, *varlist): @@ -129,8 +158,8 @@ class IfChangedNode(Node): self._last_seen = None self._varlist = varlist - def render(self, context): - if context.has_key('forloop') and context['forloop']['first']: + def iter_render(self, context): + if 'forloop' in context and context['forloop']['first']: self._last_seen = None try: if self._varlist: @@ -140,18 +169,16 @@ class IfChangedNode(Node): else: compare_to = self.nodelist.render(context) except VariableDoesNotExist: - compare_to = None + compare_to = None if compare_to != self._last_seen: firstloop = (self._last_seen == None) self._last_seen = compare_to context.push() context['ifchanged'] = {'firstloop': firstloop} - content = self.nodelist.render(context) + for chunk in self.nodelist.iter_render(context): + yield chunk context.pop() - return content - else: - return '' class IfEqualNode(Node): def __init__(self, var1, var2, nodelist_true, nodelist_false, negate): @@ -162,7 +189,7 @@ class IfEqualNode(Node): def __repr__(self): return "<IfEqualNode>" - def render(self, context): + def iter_render(self, context): try: val1 = resolve_variable(self.var1, context) except VariableDoesNotExist: @@ -172,8 +199,8 @@ class IfEqualNode(Node): except VariableDoesNotExist: val2 = None if (self.negate and val1 != val2) or (not self.negate and val1 == val2): - return self.nodelist_true.render(context) - return self.nodelist_false.render(context) + return self.nodelist_true.iter_render(context) + return self.nodelist_false.iter_render(context) class IfNode(Node): def __init__(self, bool_exprs, nodelist_true, nodelist_false, link_type): @@ -198,7 +225,7 @@ class IfNode(Node): nodes.extend(self.nodelist_false.get_nodes_by_type(nodetype)) return nodes - def render(self, context): + def iter_render(self, context): if self.link_type == IfNode.LinkTypes.or_: for ifnot, bool_expr in self.bool_exprs: try: @@ -206,8 +233,8 @@ class IfNode(Node): except VariableDoesNotExist: value = None if (value and not ifnot) or (ifnot and not value): - return self.nodelist_true.render(context) - return self.nodelist_false.render(context) + return self.nodelist_true.iter_render(context) + return self.nodelist_false.iter_render(context) else: for ifnot, bool_expr in self.bool_exprs: try: @@ -215,8 +242,8 @@ class IfNode(Node): except VariableDoesNotExist: value = None if not ((value and not ifnot) or (ifnot and not value)): - return self.nodelist_false.render(context) - return self.nodelist_true.render(context) + return self.nodelist_false.iter_render(context) + return self.nodelist_true.iter_render(context) class LinkTypes: and_ = 0, @@ -227,21 +254,16 @@ class RegroupNode(Node): self.target, self.expression = target, expression self.var_name = var_name - def render(self, context): + def iter_render(self, context): obj_list = self.target.resolve(context, True) if obj_list == None: # target_var wasn't found in context; fail silently context[self.var_name] = [] - return '' - output = [] # list of dictionaries in the format {'grouper': 'key', 'list': [list of contents]} - for obj in obj_list: - grouper = self.expression.resolve(Context({'var': obj}), True) - # TODO: Is this a sensible way to determine equality? - if output and repr(output[-1]['grouper']) == repr(grouper): - output[-1]['list'].append(obj) - else: - output.append({'grouper': grouper, 'list': [obj]}) - context[self.var_name] = output - return '' + return () + # List of dictionaries in the format + # {'grouper': 'key', 'list': [list of contents]}. + context[self.var_name] = [{'grouper':key, 'list':list(val)} for key, val in + groupby(obj_list, lambda v, f=self.expression.resolve: f(v, True))] + return () def include_is_allowed(filepath): for root in settings.ALLOWED_INCLUDE_ROOTS: @@ -253,10 +275,10 @@ class SsiNode(Node): def __init__(self, filepath, parsed): self.filepath, self.parsed = filepath, parsed - def render(self, context): + def iter_render(self, context): if not include_is_allowed(self.filepath): if settings.DEBUG: - return "[Didn't have permission to include file]" + return ("[Didn't have permission to include file]",) else: return '' # Fail silently for invalid includes. try: @@ -267,23 +289,25 @@ class SsiNode(Node): output = '' if self.parsed: try: - t = Template(output, name=self.filepath) - return t.render(context) + return Template(output, name=self.filepath).iter_render(context) except TemplateSyntaxError, e: if settings.DEBUG: return "[Included template had syntax error: %s]" % e else: return '' # Fail silently for invalid included templates. - return output + return (output,) class LoadNode(Node): - def render(self, context): - return '' + def iter_render(self, context): + return () class NowNode(Node): def __init__(self, format_string): self.format_string = format_string + def iter_render(self, context): + return (self.render(context),) + def render(self, context): from datetime import datetime from django.utils.dateformat import DateFormat @@ -312,15 +336,40 @@ class TemplateTagNode(Node): def __init__(self, tagtype): self.tagtype = tagtype + def iter_render(self, context): + return (self.render(context),) + def render(self, context): return self.mapping.get(self.tagtype, '') +class URLNode(Node): + def __init__(self, view_name, args, kwargs): + self.view_name = view_name + self.args = args + self.kwargs = kwargs + + def iter_render(self, context): + from django.core.urlresolvers import reverse, NoReverseMatch + args = [arg.resolve(context) for arg in self.args] + kwargs = dict([(k, v.resolve(context)) for k, v in self.kwargs.items()]) + try: + return (reverse(self.view_name, args=args, kwargs=kwargs),) + except NoReverseMatch: + try: + project_name = settings.SETTINGS_MODULE.split('.')[0] + return reverse(project_name + '.' + self.view_name, args=args, kwargs=kwargs) + except NoReverseMatch: + return () + class WidthRatioNode(Node): def __init__(self, val_expr, max_expr, max_width): self.val_expr = val_expr self.max_expr = max_expr self.max_width = max_width + def iter_render(self, context): + return (self.render(context),) + def render(self, context): try: value = self.val_expr.resolve(context) @@ -335,6 +384,23 @@ class WidthRatioNode(Node): return '' return str(int(round(ratio))) +class WithNode(Node): + def __init__(self, var, name, nodelist): + self.var = var + self.name = name + self.nodelist = nodelist + + def __repr__(self): + return "<WithNode>" + + def iter_render(self, context): + val = self.var.resolve(context) + context.push() + context[self.name] = val + for chunk in self.nodelist.iter_render(context): + yield chunk + context.pop() + #@register.tag def comment(parser, token): """ @@ -393,7 +459,7 @@ def cycle(parser, token): name = args[1] if not hasattr(parser, '_namedCycleNodes'): raise TemplateSyntaxError("No named cycles in template: '%s' is not defined" % name) - if not parser._namedCycleNodes.has_key(name): + if name not in parser._namedCycleNodes: raise TemplateSyntaxError("Named cycle '%s' does not exist" % name) return parser._namedCycleNodes[name] @@ -416,6 +482,15 @@ def cycle(parser, token): cycle = register.tag(cycle) def debug(parser, token): + """ + Output a whole load of debugging information, including the current context and imported modules. + + Sample usage:: + + <pre> + {% debug %} + </pre> + """ return DebugNode() debug = register.tag(debug) @@ -438,7 +513,7 @@ def do_filter(parser, token): nodelist = parser.parse(('endfilter',)) parser.delete_first_token() return FilterNode(filter_expr, nodelist) -filter = register.tag("filter", do_filter) +do_filter = register.tag("filter", do_filter) #@register.tag def firstof(parser, token): @@ -482,8 +557,14 @@ def do_for(parser, token): {% endfor %} </ul> - You can also loop over a list in reverse by using + You can loop over a list in reverse by using ``{% for obj in list reversed %}``. + + You can also unpack multiple values from a two-dimensional array:: + + {% for key,value in dict.items %} + {{ key }}: {{ value }} + {% endfor %} The for loop sets a number of variables available within the loop: @@ -504,36 +585,26 @@ def do_for(parser, token): """ bits = token.contents.split() - if len(bits) == 5 and bits[4] != 'reversed': - raise TemplateSyntaxError, "'for' statements with five words should end in 'reversed': %s" % token.contents - if len(bits) not in (4, 5): - raise TemplateSyntaxError, "'for' statements should have either four or five words: %s" % token.contents - if bits[2] != 'in': - raise TemplateSyntaxError, "'for' statement must contain 'in' as the second word: %s" % token.contents - loopvar = bits[1] - sequence = parser.compile_filter(bits[3]) - reversed = (len(bits) == 5) + if len(bits) < 4: + raise TemplateSyntaxError, "'for' statements should have at least four words: %s" % token.contents + + reversed = bits[-1] == 'reversed' + in_index = reversed and -3 or -2 + if bits[in_index] != 'in': + raise TemplateSyntaxError, "'for' statements should use the format 'for x in y': %s" % token.contents + + loopvars = re.sub(r' *, *', ',', ' '.join(bits[1:in_index])).split(',') + for var in loopvars: + if not var or ' ' in var: + raise TemplateSyntaxError, "'for' tag received an invalid argument: %s" % token.contents + + sequence = parser.compile_filter(bits[in_index+1]) nodelist_loop = parser.parse(('endfor',)) parser.delete_first_token() - return ForNode(loopvar, sequence, reversed, nodelist_loop) + return ForNode(loopvars, sequence, reversed, nodelist_loop) do_for = register.tag("for", do_for) def do_ifequal(parser, token, negate): - """ - Output the contents of the block if the two arguments equal/don't equal each other. - - Examples:: - - {% ifequal user.id comment.user_id %} - ... - {% endifequal %} - - {% ifnotequal user.id comment.user_id %} - ... - {% else %} - ... - {% endifnotequal %} - """ bits = list(token.split_contents()) if len(bits) != 3: raise TemplateSyntaxError, "%r takes two arguments" % bits[0] @@ -549,11 +620,27 @@ def do_ifequal(parser, token, negate): #@register.tag def ifequal(parser, token): + """ + Output the contents of the block if the two arguments equal each other. + + Examples:: + + {% ifequal user.id comment.user_id %} + ... + {% endifequal %} + + {% ifnotequal user.id comment.user_id %} + ... + {% else %} + ... + {% endifnotequal %} + """ return do_ifequal(parser, token, False) ifequal = register.tag(ifequal) #@register.tag def ifnotequal(parser, token): + """Output the contents of the block if the two arguments are not equal. See ifequal.""" return do_ifequal(parser, token, True) ifnotequal = register.tag(ifnotequal) @@ -566,8 +653,8 @@ def do_if(parser, token): :: - {% if althlete_list %} - Number of athletes: {{ althete_list|count }} + {% if athlete_list %} + Number of athletes: {{ athlete_list|count }} {% else %} No athletes. {% endif %} @@ -798,7 +885,7 @@ def regroup(parser, token): if lastbits_reversed[1][::-1] != 'as': raise TemplateSyntaxError, "next-to-last argument to 'regroup' tag must be 'as'" - expression = parser.compile_filter('var.%s' % lastbits_reversed[2][::-1]) + expression = parser.compile_filter(lastbits_reversed[2][::-1]) var_name = lastbits_reversed[0][::-1] return RegroupNode(target, expression, var_name) @@ -806,7 +893,7 @@ regroup = register.tag(regroup) def spaceless(parser, token): """ - Normalize whitespace between HTML tags to a single space. This includes tab + Removes whitespace between HTML tags. This includes tab characters and newlines. Example usage:: @@ -819,7 +906,7 @@ def spaceless(parser, token): This example would return this HTML:: - <p> <a href="foo/">Foo</a> </p> + <p><a href="foo/">Foo</a></p> Only space between *tags* is normalized -- not space between tags and text. In this example, the space around ``Hello`` won't be stripped:: @@ -862,12 +949,58 @@ def templatetag(parser, token): if len(bits) != 2: raise TemplateSyntaxError, "'templatetag' statement takes one argument" tag = bits[1] - if not TemplateTagNode.mapping.has_key(tag): + if tag not in TemplateTagNode.mapping: raise TemplateSyntaxError, "Invalid templatetag argument: '%s'. Must be one of: %s" % \ (tag, TemplateTagNode.mapping.keys()) return TemplateTagNode(tag) templatetag = register.tag(templatetag) +def url(parser, token): + """ + Returns an absolute URL matching given view with its parameters. + + This is a way to define links that aren't tied to a particular URL configuration:: + + {% url path.to.some_view arg1,arg2,name1=value1 %} + + The first argument is a path to a view. It can be an absolute python path + or just ``app_name.view_name`` without the project name if the view is + located inside the project. Other arguments are comma-separated values + that will be filled in place of positional and keyword arguments in the + URL. All arguments for the URL should be present. + + For example if you have a view ``app_name.client`` taking client's id and + the corresponding line in a URLconf looks like this:: + + ('^client/(\d+)/$', 'app_name.client') + + and this app's URLconf is included into the project's URLconf under some + path:: + + ('^clients/', include('project_name.app_name.urls')) + + then in a template you can create a link for a certain client like this:: + + {% url app_name.client client.id %} + + The URL will look like ``/clients/client/123/``. + """ + bits = token.contents.split(' ', 2) + if len(bits) < 2: + raise TemplateSyntaxError, "'%s' takes at least one argument (path to a view)" % bits[0] + args = [] + kwargs = {} + if len(bits) > 2: + for arg in bits[2].split(','): + if '=' in arg: + k, v = arg.split('=', 1) + k = k.strip() + kwargs[k] = parser.compile_filter(v) + else: + args.append(parser.compile_filter(arg)) + return URLNode(bits[1], args, kwargs) +url = register.tag(url) + #@register.tag def widthratio(parser, token): """ @@ -893,3 +1026,25 @@ def widthratio(parser, token): return WidthRatioNode(parser.compile_filter(this_value_expr), parser.compile_filter(max_value_expr), max_width) widthratio = register.tag(widthratio) + +#@register.tag +def do_with(parser, token): + """ + Add a value to the context (inside of this block) for caching and easy + access. + + For example:: + + {% with person.some_sql_method as total %} + {{ total }} object{{ total|pluralize }} + {% endwith %} + """ + bits = list(token.split_contents()) + if len(bits) != 4 or bits[2] != "as": + raise TemplateSyntaxError, "%r expected format is 'value as name'" % bits[0] + var = parser.compile_filter(bits[1]) + name = bits[3] + nodelist = parser.parse(('endwith',)) + parser.delete_first_token() + return WithNode(var, name, nodelist) +do_with = register.tag('with', do_with) diff --git a/django/template/loader.py b/django/template/loader.py index 03e6f8d49d..45cf5a9d7c 100644 --- a/django/template/loader.py +++ b/django/template/loader.py @@ -87,14 +87,12 @@ def get_template_from_string(source, origin=None, name=None): """ return Template(source, origin, name) -def render_to_string(template_name, dictionary=None, context_instance=None): +def _render_setup(template_name, dictionary=None, context_instance=None): """ - Loads the given template_name and renders it with the given dictionary as - context. The template_name may be a string to load a single template using - get_template, or it may be a tuple to use select_template to find one of - the templates in the list. Returns a string. + Common setup code for render_to_string and render_to_iter. """ - dictionary = dictionary or {} + if dictionary is None: + dictionary = {} if isinstance(template_name, (list, tuple)): t = select_template(template_name) else: @@ -103,7 +101,28 @@ def render_to_string(template_name, dictionary=None, context_instance=None): context_instance.update(dictionary) else: context_instance = Context(dictionary) - return t.render(context_instance) + return t, context_instance + +def render_to_string(template_name, dictionary=None, context_instance=None): + """ + Loads the given template_name and renders it with the given dictionary as + context. The template_name may be a string to load a single template using + get_template, or it may be a tuple to use select_template to find one of + the templates in the list. Returns a string. + """ + t, c = _render_setup(template_name, dictionary=dictionary, context_instance=context_instance) + return t.render(c) + +def render_to_iter(template_name, dictionary=None, context_instance=None): + """ + Loads the given template_name and renders it with the given dictionary as + context. The template_name may be a string to load a single template using + get_template, or it may be a tuple to use select_template to find one of + the templates in the list. Returns a string. + """ + t, c = _render_setup(template_name, dictionary=dictionary, context_instance=context_instance) + return t.iter_render(c) + def select_template(template_name_list): "Given a list of template names, returns the first that can be loaded." diff --git a/django/template/loader_tags.py b/django/template/loader_tags.py index e329b1bb36..d12d0b55ad 100644 --- a/django/template/loader_tags.py +++ b/django/template/loader_tags.py @@ -15,14 +15,14 @@ class BlockNode(Node): def __repr__(self): return "<Block Node: %s. Contents: %r>" % (self.name, self.nodelist) - def render(self, context): + def iter_render(self, context): context.push() # Save context in case of block.super(). self.context = context context['block'] = self - result = self.nodelist.render(context) + for chunk in self.nodelist.iter_render(context): + yield chunk context.pop() - return result def super(self): if self.parent: @@ -59,7 +59,7 @@ class ExtendsNode(Node): else: return get_template_from_string(source, origin, parent) - def render(self, context): + def iter_render(self, context): compiled_parent = self.get_parent(context) parent_is_child = isinstance(compiled_parent.nodelist[0], ExtendsNode) parent_blocks = dict([(n.name, n) for n in compiled_parent.nodelist.get_nodes_by_type(BlockNode)]) @@ -79,7 +79,7 @@ class ExtendsNode(Node): parent_block.parent = block_node.parent parent_block.add_parent(parent_block.nodelist) parent_block.nodelist = block_node.nodelist - return compiled_parent.render(context) + return compiled_parent.iter_render(context) class ConstantIncludeNode(Node): def __init__(self, template_path): @@ -91,27 +91,26 @@ class ConstantIncludeNode(Node): raise self.template = None - def render(self, context): + def iter_render(self, context): if self.template: - return self.template.render(context) - else: - return '' + return self.template.iter_render(context) + return () class IncludeNode(Node): def __init__(self, template_name): self.template_name = template_name - def render(self, context): + def iter_render(self, context): try: template_name = resolve_variable(self.template_name, context) t = get_template(template_name) - return t.render(context) + return t.iter_render(context) except TemplateSyntaxError, e: if settings.TEMPLATE_DEBUG: raise - return '' + return () except: - return '' # Fail silently for invalid included templates. + return () # Fail silently for invalid included templates. def do_block(parser, token): """ @@ -129,7 +128,7 @@ def do_block(parser, token): parser.__loaded_blocks.append(block_name) except AttributeError: # parser.__loaded_blocks isn't a list yet parser.__loaded_blocks = [block_name] - nodelist = parser.parse(('endblock',)) + nodelist = parser.parse(('endblock', 'endblock %s' % block_name)) parser.delete_first_token() return BlockNode(block_name, nodelist) |
