Permalink
Browse files

Fixes #7817 and #9456.

- The include tag now has a 'with' option to include to provide extra context
  vairables to the included template.

- The include tag now has an 'only' option to exclude the current context
  when rendering the included template.

- The with tag now accepts multiple variable assignments.

- The with, include and blocktrans tags now use a new keyword argument format
  for variable assignments (e.g. `{% with foo=1 bar=2 %}`).

git-svn-id: http://code.djangoproject.com/svn/django/trunk@14922 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
1 parent 99742d8 commit 3ae9117c467f9fabed8736949dee209d40293b8d @SmileyChris SmileyChris committed Dec 18, 2010
@@ -16,6 +16,55 @@
# Regex for token keyword arguments
kwarg_re = re.compile(r"(?:(\w+)=)?(.+)")
+def token_kwargs(bits, parser, support_legacy=False):
+ """
+ A utility method for parsing token keyword arguments.
+
+ :param bits: A list containing remainder of the token (split by spaces)
+ that is to be checked for arguments. Valid arguments will be removed
+ from this list.
+
+ :param support_legacy: If set to true ``True``, the legacy format
+ ``1 as foo`` will be accepted. Otherwise, only the standard ``foo=1``
+ format is allowed.
+
+ :returns: A dictionary of the arguments retrieved from the ``bits`` token
+ list.
+
+ There is no requirement for all remaining token ``bits`` to be keyword
+ arguments, so the dictionary will be returned as soon as an invalid
+ argument format is reached.
+ """
+ if not bits:
+ return {}
+ match = kwarg_re.match(bits[0])
+ kwarg_format = match and match.group(1)
+ if not kwarg_format:
+ if not support_legacy:
+ return {}
+ if len(bits) < 3 or bits[1] != 'as':
+ return {}
+
+ kwargs = {}
+ while bits:
+ if kwarg_format:
+ match = kwarg_re.match(bits[0])
+ if not match or not match.group(1):
+ return kwargs
+ key, value = match.groups()
+ del bits[:1]
+ else:
+ if len(bits) < 3 or bits[1] != 'as':
+ return kwargs
+ key, value = bits[2], bits[0]
+ del bits[:3]
+ kwargs[key] = parser.compile_filter(value)
+ if bits and not kwarg_format:
+ if bits[0] != 'and':
+ return kwargs
+ del bits[:1]
+ return kwargs
+
class AutoEscapeControlNode(Node):
"""Implements the actions of the autoescape tag."""
def __init__(self, setting, nodelist):
@@ -298,7 +347,7 @@ def __init__(self, filepath, parsed, legacy_filepath=True):
def render(self, context):
filepath = self.filepath
if not self.legacy_filepath:
- filepath = filepath.resolve(context)
+ filepath = filepath.resolve(context)
if not include_is_allowed(filepath):
if settings.DEBUG:
@@ -433,18 +482,25 @@ def render(self, context):
return str(int(round(ratio)))
class WithNode(Node):
- def __init__(self, var, name, nodelist):
- self.var = var
- self.name = name
+ def __init__(self, var, name, nodelist, extra_context=None,
+ isolated_context=False):
self.nodelist = nodelist
+ # var and name are legacy attributes, being left in case they are used
+ # by third-party subclasses of this Node.
+ self.extra_context = extra_context or {}
+ if name:
+ self.extra_context[name] = var
+ self.isolated_context = isolated_context
def __repr__(self):
return "<WithNode>"
def render(self, context):
- val = self.var.resolve(context)
- context.push()
- context[self.name] = val
+ values = dict([(key, val.resolve(context)) for key, val in
+ self.extra_context.iteritems()])
+ if self.isolated_context:
+ return self.nodelist.render(Context(values))
+ context.update(values)
output = self.nodelist.render(context)
context.pop()
return output
@@ -1276,22 +1332,34 @@ def widthratio(parser, token):
#@register.tag
def do_with(parser, token):
"""
- Adds a value to the context (inside of this block) for caching and easy
- access.
+ Adds one or more values to the context (inside of this block) for caching
+ and easy access.
For example::
- {% with person.some_sql_method as total %}
+ {% with total=person.some_sql_method %}
{{ total }} object{{ total|pluralize }}
{% endwith %}
+
+ Multiple values can be added to the context::
+
+ {% with foo=1 bar=2 %}
+ ...
+ {% endwith %}
+
+ The legacy format of ``{% with person.some_sql_method as total %}`` is
+ still accepted.
"""
- 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]
+ bits = token.split_contents()
+ remaining_bits = bits[1:]
+ extra_context = token_kwargs(remaining_bits, parser, support_legacy=True)
+ if not extra_context:
+ raise TemplateSyntaxError("%r expected at least one variable "
+ "assignment" % bits[0])
+ if remaining_bits:
+ raise TemplateSyntaxError("%r received an invalid token: %r" %
+ (bits[0], remaining_bits[0]))
nodelist = parser.parse(('endwith',))
parser.delete_first_token()
- return WithNode(var, name, nodelist)
+ return WithNode(None, None, nodelist, extra_context=extra_context)
do_with = register.tag('with', do_with)
@@ -1,5 +1,7 @@
from django.template.base import TemplateSyntaxError, TemplateDoesNotExist, Variable
from django.template.base import Library, Node, TextNode
+from django.template.context import Context
+from django.template.defaulttags import token_kwargs
from django.template.loader import get_template
from django.conf import settings
from django.utils.safestring import mark_safe
@@ -124,8 +126,25 @@ def render(self, context):
# the same.
return compiled_parent._render(context)
-class ConstantIncludeNode(Node):
- def __init__(self, template_path):
+class BaseIncludeNode(Node):
+ def __init__(self, *args, **kwargs):
+ self.extra_context = kwargs.pop('extra_context', {})
+ self.isolated_context = kwargs.pop('isolated_context', False)
+ super(BaseIncludeNode, self).__init__(*args, **kwargs)
+
+ def render_template(self, template, context):
+ values = dict([(name, var.resolve(context)) for name, var
+ in self.extra_context.iteritems()])
+ if self.isolated_context:
+ return template.render(Context(values))
+ context.update(values)
+ output = template.render(context)
+ context.pop()
+ return output
+
+class ConstantIncludeNode(BaseIncludeNode):
+ def __init__(self, template_path, *args, **kwargs):
+ super(ConstantIncludeNode, self).__init__(*args, **kwargs)
try:
t = get_template(template_path)
self.template = t
@@ -135,21 +154,21 @@ def __init__(self, template_path):
self.template = None
def render(self, context):
- if self.template:
- return self.template.render(context)
- else:
+ if not self.template:
return ''
+ return self.render_template(self.template, context)
-class IncludeNode(Node):
- def __init__(self, template_name):
- self.template_name = Variable(template_name)
+class IncludeNode(BaseIncludeNode):
+ def __init__(self, template_name, *args, **kwargs):
+ super(IncludeNode, self).__init__(*args, **kwargs)
+ self.template_name = template_name
def render(self, context):
try:
template_name = self.template_name.resolve(context)
- t = get_template(template_name)
- return t.render(context)
- except TemplateSyntaxError, e:
+ template = get_template(template_name)
+ return self.render_template(template, context)
+ except TemplateSyntaxError:
if settings.TEMPLATE_DEBUG:
raise
return ''
@@ -201,19 +220,49 @@ def do_extends(parser, token):
def do_include(parser, token):
"""
- Loads a template and renders it with the current context.
+ Loads a template and renders it with the current context. You can pass
+ additional context using keyword arguments.
Example::
{% include "foo/some_include" %}
+ {% include "foo/some_include" with bar="BAZZ!" baz="BING!" %}
+
+ Use the ``only`` argument to exclude the current context when rendering
+ the included template::
+
+ {% include "foo/some_include" only %}
+ {% include "foo/some_include" with bar="1" only %}
"""
bits = token.split_contents()
- if len(bits) != 2:
- raise TemplateSyntaxError("%r tag takes one argument: the name of the template to be included" % bits[0])
+ if len(bits) < 2:
+ raise TemplateSyntaxError("%r tag takes at least one argument: the name of the template to be included." % bits[0])
+ options = {}
+ remaining_bits = bits[2:]
+ while remaining_bits:
+ option = remaining_bits.pop(0)
+ if option in options:
+ raise TemplateSyntaxError('The %r option was specified more '
+ 'than once.' % option)
+ if option == 'with':
+ value = token_kwargs(remaining_bits, parser, support_legacy=False)
+ if not value:
+ raise TemplateSyntaxError('"with" in %r tag needs at least '
+ 'one keyword argument.' % bits[0])
+ elif option == 'only':
+ value = True
+ else:
+ raise TemplateSyntaxError('Unknown argument for %r tag: %r.' %
+ (bits[0], option))
+ options[option] = value
+ isolated_context = options.get('only', False)
+ namemap = options.get('with', {})
path = bits[1]
if path[0] in ('"', "'") and path[-1] == path[0]:
- return ConstantIncludeNode(path[1:-1])
- return IncludeNode(bits[1])
+ return ConstantIncludeNode(path[1:-1], extra_context=namemap,
+ isolated_context=isolated_context)
+ return IncludeNode(parser.compile_filter(bits[1]), extra_context=namemap,
+ isolated_context=isolated_context)
register.tag('block', do_block)
register.tag('extends', do_extends)
@@ -6,6 +6,7 @@
from django.template.base import _render_value_in_context
from django.utils import translation
from django.utils.encoding import force_unicode
+from django.template.defaulttags import token_kwargs
register = Library()
@@ -97,7 +98,7 @@ def render_token_list(self, tokens):
def render(self, context):
tmp_context = {}
for var, val in self.extra_context.items():
- tmp_context[var] = val.render(context)
+ tmp_context[var] = val.resolve(context)
# Update() works like a push(), so corresponding context.pop() is at
# the end of function
context.update(tmp_context)
@@ -284,43 +285,54 @@ def do_block_translate(parser, token):
Usage::
- {% blocktrans with foo|filter as bar and baz|filter as boo %}
+ {% blocktrans with bar=foo|filter boo=baz|filter %}
This is {{ bar }} and {{ boo }}.
{% endblocktrans %}
Additionally, this supports pluralization::
- {% blocktrans count var|length as count %}
+ {% blocktrans count count=var|length %}
There is {{ count }} object.
{% plural %}
There are {{ count }} objects.
{% endblocktrans %}
This is much like ngettext, only in template syntax.
- """
- class BlockTranslateParser(TokenParser):
- def top(self):
- countervar = None
- counter = None
- extra_context = {}
- while self.more():
- tag = self.tag()
- if tag == 'with' or tag == 'and':
- value = self.value()
- if self.tag() != 'as':
- raise TemplateSyntaxError("variable bindings in 'blocktrans' must be 'with value as variable'")
- extra_context[self.tag()] = VariableNode(
- parser.compile_filter(value))
- elif tag == 'count':
- counter = parser.compile_filter(self.value())
- if self.tag() != 'as':
- raise TemplateSyntaxError("counter specification in 'blocktrans' must be 'count value as variable'")
- countervar = self.tag()
- else:
- raise TemplateSyntaxError("unknown subtag %s for 'blocktrans' found" % tag)
- return (countervar, counter, extra_context)
- countervar, counter, extra_context = BlockTranslateParser(token.contents).top()
+ The "var as value" legacy format is still supported::
+
+ {% blocktrans with foo|filter as bar and baz|filter as boo %}
+ {% blocktrans count var|length as count %}
+ """
+ bits = token.split_contents()
+
+ options = {}
+ remaining_bits = bits[1:]
+ while remaining_bits:
+ option = remaining_bits.pop(0)
+ if option in options:
+ raise TemplateSyntaxError('The %r option was specified more '
+ 'than once.' % option)
+ if option == 'with':
+ value = token_kwargs(remaining_bits, parser, support_legacy=True)
+ if not value:
+ raise TemplateSyntaxError('"with" in %r tag needs at least '
+ 'one keyword argument.' % bits[0])
+ elif option == 'count':
+ value = token_kwargs(remaining_bits, parser, support_legacy=True)
+ if len(value) != 1:
+ raise TemplateSyntaxError('"count" in %r tag expected exactly '
+ 'one keyword argument.' % bits[0])
+ else:
+ raise TemplateSyntaxError('Unknown argument for %r tag: %r.' %
+ (bits[0], option))
+ options[option] = value
+
+ if 'count' in options:
+ countervar, counter = options['count'].items()[0]
+ else:
+ countervar, counter = None, None
+ extra_context = options.get('with', {})
singular = []
plural = []
Oops, something went wrong.

0 comments on commit 3ae9117

Please sign in to comment.