Skip to content

Commit

Permalink
Fixes #7817 and #9456.
Browse files Browse the repository at this point in the history
- 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
SmileyChris committed Dec 18, 2010
1 parent 99742d8 commit 3ae9117
Show file tree
Hide file tree
Showing 8 changed files with 274 additions and 83 deletions.
102 changes: 85 additions & 17 deletions django/template/defaulttags.py
Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
81 changes: 65 additions & 16 deletions django/template/loader_tags.py
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 ''
Expand Down Expand Up @@ -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)
Expand Down
64 changes: 38 additions & 26 deletions django/templatetags/i18n.py
Expand Up @@ -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()

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 = []
Expand Down

0 comments on commit 3ae9117

Please sign in to comment.