Implemented 'smart if' template tag, allowing filters and various operators to be used in the 'if' tag

Thanks to Chris Beaven for the initial patch, Fredrik Lundh for the basis
of the parser methodology and Russell Keith-Magee for code reviews.

There are some BACKWARDS INCOMPATIBILITIES in rare cases - in particular, if
you were using the keywords 'and', 'or' or 'not' as variable names within
the 'if' expression, which was previously allowed in some cases.



git-svn-id: http://code.djangoproject.com/svn/django/trunk@11806 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Luke Plant 2009-12-09 22:40:36 +00:00
parent 25020ddb05
commit 2c2f5aee4d
7 changed files with 514 additions and 93 deletions

View File

@ -11,6 +11,7 @@ except NameError:
from django.template import Node, NodeList, Template, Context, 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.template.smartif import IfParser, Literal
from django.conf import settings
from django.utils.encoding import smart_str, smart_unicode
from django.utils.itercompat import groupby
@ -227,10 +228,9 @@ class IfEqualNode(Node):
return self.nodelist_false.render(context)
class IfNode(Node):
def __init__(self, bool_exprs, nodelist_true, nodelist_false, link_type):
self.bool_exprs = bool_exprs
def __init__(self, var, nodelist_true, nodelist_false=None):
self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false
self.link_type = link_type
self.var = var
def __repr__(self):
return "<If node>"
@ -250,28 +250,10 @@ class IfNode(Node):
return nodes
def render(self, context):
if self.link_type == IfNode.LinkTypes.or_:
for ifnot, bool_expr in self.bool_exprs:
try:
value = bool_expr.resolve(context, True)
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)
else:
for ifnot, bool_expr in self.bool_exprs:
try:
value = bool_expr.resolve(context, True)
except VariableDoesNotExist:
value = None
if not ((value and not ifnot) or (ifnot and not value)):
return self.nodelist_false.render(context)
if self.var.eval(context):
return self.nodelist_true.render(context)
class LinkTypes:
and_ = 0,
or_ = 1
else:
return self.nodelist_false.render(context)
class RegroupNode(Node):
def __init__(self, target, expression, var_name):
@ -761,6 +743,27 @@ def ifnotequal(parser, token):
return do_ifequal(parser, token, True)
ifnotequal = register.tag(ifnotequal)
class TemplateLiteral(Literal):
def __init__(self, value, text):
self.value = value
self.text = text # for better error messages
def display(self):
return self.text
def eval(self, context):
return self.value.resolve(context, ignore_failures=True)
class TemplateIfParser(IfParser):
error_class = TemplateSyntaxError
def __init__(self, parser, *args, **kwargs):
self.template_parser = parser
return super(TemplateIfParser, self).__init__(*args, **kwargs)
def create_var(self, value):
return TemplateLiteral(self.template_parser.compile_filter(value), value)
#@register.tag(name="if")
def do_if(parser, token):
"""
@ -805,47 +808,21 @@ def do_if(parser, token):
There are some athletes and absolutely no coaches.
{% endif %}
``if`` tags do not allow ``and`` and ``or`` clauses with the same tag,
because the order of logic would be ambigous. For example, this is
invalid::
Comparison operators are also available, and the use of filters is also
allowed, for example:
{% if athlete_list and coach_list or cheerleader_list %}
{% if articles|length >= 5 %}...{% endif %}
If you need to combine ``and`` and ``or`` to do advanced logic, just use
nested if tags. For example::
Arguments and operators _must_ have a space between them, so
``{% if 1>2 %}`` is not a valid if tag.
{% if athlete_list %}
{% if coach_list or cheerleader_list %}
We have athletes, and either coaches or cheerleaders!
{% endif %}
{% endif %}
All supported operators are: ``or``, ``and``, ``in``, ``==`` (or ``=``),
``!=``, ``>``, ``>=``, ``<`` and ``<=``.
Operator precedence follows Python.
"""
bits = token.contents.split()
del bits[0]
if not bits:
raise TemplateSyntaxError("'if' statement requires at least one argument")
# Bits now looks something like this: ['a', 'or', 'not', 'b', 'or', 'c.d']
bitstr = ' '.join(bits)
boolpairs = bitstr.split(' and ')
boolvars = []
if len(boolpairs) == 1:
link_type = IfNode.LinkTypes.or_
boolpairs = bitstr.split(' or ')
else:
link_type = IfNode.LinkTypes.and_
if ' or ' in bitstr:
raise TemplateSyntaxError, "'if' tags can't mix 'and' and 'or'"
for boolpair in boolpairs:
if ' ' in boolpair:
try:
not_, boolvar = boolpair.split()
except ValueError:
raise TemplateSyntaxError, "'if' statement improperly formatted"
if not_ != 'not':
raise TemplateSyntaxError, "Expected 'not' in if statement"
boolvars.append((True, parser.compile_filter(boolvar)))
else:
boolvars.append((False, parser.compile_filter(boolpair)))
bits = token.split_contents()[1:]
var = TemplateIfParser(parser, bits).parse()
nodelist_true = parser.parse(('else', 'endif'))
token = parser.next_token()
if token.contents == 'else':
@ -853,7 +830,7 @@ def do_if(parser, token):
parser.delete_first_token()
else:
nodelist_false = NodeList()
return IfNode(boolvars, nodelist_true, nodelist_false, link_type)
return IfNode(var, nodelist_true, nodelist_false)
do_if = register.tag("if", do_if)
#@register.tag

192
django/template/smartif.py Normal file
View File

@ -0,0 +1,192 @@
"""
Parser and utilities for the smart 'if' tag
"""
import operator
# Using a simple top down parser, as described here:
# http://effbot.org/zone/simple-top-down-parsing.htm.
# 'led' = left denotation
# 'nud' = null denotation
# 'bp' = binding power (left = lbp, right = rbp)
class TokenBase(object):
"""
Base class for operators and literals, mainly for debugging and for throwing
syntax errors.
"""
id = None # node/token type name
value = None # used by literals
first = second = None # used by tree nodes
def nud(self, parser):
# Null denotation - called in prefix context
raise parser.error_class(
"Not expecting '%s' in this position in if tag." % self.id
)
def led(self, left, parser):
# Left denotation - called in infix context
raise parser.error_class(
"Not expecting '%s' as infix operator in if tag." % self.id
)
def display(self):
"""
Returns what to display in error messages for this node
"""
return self.id
def __repr__(self):
out = [str(x) for x in [self.id, self.first, self.second] if x is not None]
return "(" + " ".join(out) + ")"
def infix(bp, func):
"""
Creates an infix operator, given a binding power and a function that
evaluates the node
"""
class Operator(TokenBase):
lbp = bp
def led(self, left, parser):
self.first = left
self.second = parser.expression(bp)
return self
def eval(self, context):
try:
return func(self.first.eval(context), self.second.eval(context))
except Exception:
# Templates shouldn't throw exceptions when rendering. We are
# most likely to get exceptions for things like {% if foo in bar
# %} where 'bar' does not support 'in', so default to False
return False
return Operator
def prefix(bp, func):
"""
Creates a prefix operator, given a binding power and a function that
evaluates the node.
"""
class Operator(TokenBase):
lbp = bp
def nud(self, parser):
self.first = parser.expression(bp)
self.second = None
return self
def eval(self, context):
try:
return func(self.first.eval(context))
except Exception:
return False
return Operator
# Operator precedence follows Python.
# NB - we can get slightly more accurate syntax error messages by not using the
# same object for '==' and '='.
OPERATORS = {
'or': infix(6, lambda x, y: x or y),
'and': infix(7, lambda x, y: x and y),
'not': prefix(8, operator.not_),
'in': infix(9, lambda x, y: x in y),
'=': infix(10, operator.eq),
'==': infix(10, operator.eq),
'!=': infix(10, operator.ne),
'>': infix(10, operator.gt),
'>=': infix(10, operator.ge),
'<': infix(10, operator.lt),
'<=': infix(10, operator.le),
}
# Assign 'id' to each:
for key, op in OPERATORS.items():
op.id = key
class Literal(TokenBase):
"""
A basic self-resolvable object similar to a Django template variable.
"""
# IfParser uses Literal in create_var, but TemplateIfParser overrides
# create_var so that a proper implementation that actually resolves
# variables, filters etc is used.
id = "literal"
lbp = 0
def __init__(self, value):
self.value = value
def display(self):
return repr(self.value)
def nud(self, parser):
return self
def eval(self, context):
return self.value
def __repr__(self):
return "(%s %r)" % (self.id, self.value)
class EndToken(TokenBase):
lbp = 0
def nud(self, parser):
raise parser.error_class("Unexpected end of expression in if tag.")
EndToken = EndToken()
class IfParser(object):
error_class = ValueError
def __init__(self, tokens):
self.tokens = map(self.translate_tokens, tokens)
self.pos = 0
self.current_token = self.next()
def translate_tokens(self, token):
try:
op = OPERATORS[token]
except (KeyError, TypeError):
return self.create_var(token)
else:
return op()
def next(self):
if self.pos >= len(self.tokens):
return EndToken
else:
retval = self.tokens[self.pos]
self.pos += 1
return retval
def parse(self):
retval = self.expression()
# Check that we have exhausted all the tokens
if self.current_token is not EndToken:
raise self.error_class("Unused '%s' at end of if expression." %
self.current_token.display())
return retval
def expression(self, rbp=0):
t = self.current_token
self.current_token = self.next()
left = t.nud(self)
while rbp < self.current_token.lbp:
t = self.current_token
self.current_token = self.next()
left = t.led(left, self)
return left
def create_var(self, value):
return Literal(value)

View File

@ -313,6 +313,9 @@ displayed by the ``{{ athlete_list|length }}`` variable.
As you can see, the ``if`` tag can take an optional ``{% else %}`` clause that
will be displayed if the test fails.
Boolean operators
^^^^^^^^^^^^^^^^^
``if`` tags may use ``and``, ``or`` or ``not`` to test a number of variables or
to negate a given variable::
@ -338,24 +341,153 @@ to negate a given variable::
There are some athletes and absolutely no coaches.
{% endif %}
``if`` tags don't allow ``and`` and ``or`` clauses within the same tag, because
the order of logic would be ambiguous. For example, this is invalid::
.. versionchanged:: 1.2
Use of both ``and`` and ``or`` clauses within the same tag is allowed, with
``and`` having higher precedence than ``or`` e.g.::
{% if athlete_list and coach_list or cheerleader_list %}
If you need to combine ``and`` and ``or`` to do advanced logic, just use nested
``if`` tags. For example::
will be interpreted like:
{% if athlete_list %}
{% if coach_list or cheerleader_list %}
We have athletes, and either coaches or cheerleaders!
{% endif %}
.. code-block:: python
if (athlete_list and coach_list) or cheerleader_list
Use of actual brackets in the ``if`` tag is invalid syntax. If you need them to
indicate precedence, you should use nested ``if`` tags.
.. versionadded:: 1.2
``if`` tags may also use the operators ``==``, ``!=``, ``<``, ``>``,
``<=``, ``>=`` and ``in`` which work as follows:
``==`` operator
^^^^^^^^^^^^^^^
Equality. Example::
{% if somevar == "x" %}
This appears if variable somevar equals the string "x"
{% endif %}
Multiple uses of the same logical operator are fine, as long as you use the
same operator. For example, this is valid::
``!=`` operator
^^^^^^^^^^^^^^^
Inequality. Example::
{% if somevar != "x" %}
This appears if variable somevar does not equal the string "x",
or if somevar is not found in the context
{% endif %}
``<`` operator
^^^^^^^^^^^^^^
Less than. Example::
{% if somevar < 100 %}
This appears if variable somevar is less than 100.
{% endif %}
``>`` operator
^^^^^^^^^^^^^^
Greater than. Example::
{% if somevar > 0 %}
This appears if variable somevar is greater than 0.
{% endif %}
``<=`` operator
^^^^^^^^^^^^^^^
Less than or equal to. Example::
{% if somevar <= 100 %}
This appears if variable somevar is less than 100 or equal to 100.
{% endif %}
``>=`` operator
^^^^^^^^^^^^^^^
Greater than or equal to. Example::
{% if somevar >= 1 %}
This appears if variable somevar is greater than 1 or equal to 1.
{% endif %}
``in`` operator
^^^^^^^^^^^^^^^
Contained within. This operator is supported by many Python containers to test
whether the given value is in the container. The following are some examples of
how ``x in y`` will be interpreted::
{% if "bc" in "abcdef" %}
This appears since "bc" is a substring of "abcdef"
{% endif %}
{% if "hello" in greetings %}
If greetings is a list or set, one element of which is the string
"hello", this will appear.
{% endif %}
{% if user in users %}
If users is a QuerySet, this will appear if user is an
instance that belongs to the QuerySet.
{% endif %}
The comparison operators cannot be 'chained' like in Python or in mathematical
notation. For example, instead of using::
{% if a > b > c %} (WRONG)
you should use::
{% if a > b and b > c %}
Filters
^^^^^^^
You can also use filters in the ``if`` expression. For example::
{% if messages|length >= 100 %}
You have lots of messages today!
{% endif %}
Complex expressions
^^^^^^^^^^^^^^^^^^^
All of the above can be combined to form complex expressions. For such
expressions, it can be important to know how the operators are grouped when the
expression is evaluated - that is, the precedence rules. The precedence of the
operators, from lowest to highest, is as follows:
* ``or``
* ``and``
* ``not``
* ``in``
* ``==``, ``!=``, ``<``, ``>``,``<=``, ``>=``
(This follows Python exactly). So, for example, the following complex if tag:
{% if a == b or c == d and e %}
...will be interpreted as:
.. code-block:: python
(a == b) or ((c == d) and e)
If you need different precedence, you will need to use nested if tags. Sometimes
that is better for clarity anyway, for the sake of those who do not know the
precedence rules.
{% if athlete_list or coach_list or parent_list or teacher_list %}
.. templatetag:: ifchanged
@ -427,6 +559,9 @@ You cannot check for equality with Python objects such as ``True`` or
``False``. If you need to test if something is true or false, use the ``if``
tag instead.
.. versionadded:: 1.2
An alternative to the ``ifequal`` tag is to use the :ttag:`if` tag and the ``==`` operator.
.. templatetag:: ifnotequal
ifnotequal
@ -434,6 +569,9 @@ ifnotequal
Just like ``ifequal``, except it tests that the two arguments are not equal.
.. versionadded:: 1.2
An alternative to the ``ifnotequal`` tag is to use the :ttag:`if` tag and the ``!=`` operator.
.. templatetag:: include
include

View File

@ -42,6 +42,15 @@ changes that developers must be aware of:
* All of the CSRF has moved from contrib to core (with backwards compatible
imports in the old locations, which are deprecated).
:ttag:`if` tag changes
----------------------
Due to new features in the :ttag:`if` template tag, it no longer accepts 'and',
'or' and 'not' as valid **variable** names. Previously that worked in some
cases even though these strings were normally treated as keywords. Now, the
keyword status is always enforced, and template code like ``{% if not %}`` or
``{% if and %}`` will throw a TemplateSyntaxError.
``LazyObject``
--------------
@ -196,3 +205,37 @@ messaging, for both anonymous and authenticated clients. The messages framework
replaces the deprecated user message API and allows you to temporarily store
messages in one request and retrieve them for display in a subsequent request
(usually the next one).
'Smart' if tag
--------------
The :ttag:`if` tag has been upgraded to be much more powerful. First, support
for comparison operators has been added. No longer will you have to type:
.. code-block:: html+django
{% ifnotequal a b %}
...
{% endifnotequal %}
...as you can now do:
.. code-block:: html+django
{% if a != b %}
...
{% endif %}
The operators supported are ``==``, ``!=``, ``<``, ``>``, ``<=``, ``>=`` and
``in``, all of which work like the Python operators, in addition to ``and``,
``or`` and ``not`` which were already supported.
Also, filters may now be used in the ``if`` expression. For example:
.. code-block:: html+django
<div
{% if user.email|lower == message.recipient|lower %}
class="highlight"
{% endif %}
>{{ message }}</div>

View File

@ -188,7 +188,7 @@ tags:
{% endfor %}
</ul>
:ttag:`if` and :ttag:`else`
:ttag:`if` and ``else``
Evaluates a variable, and if that variable is "true" the contents of the
block are displayed::
@ -201,18 +201,13 @@ tags:
In the above, if ``athlete_list`` is not empty, the number of athletes
will be displayed by the ``{{ athlete_list|length }}`` variable.
:ttag:`ifequal` and :ttag:`ifnotequal`
Display some contents if two arguments are or are not equal. For example::
You can also use filters and various operators in the ``if`` tag::
{% ifequal athlete.name coach.name %}
...
{% endifequal %}
Or::
{% ifnotequal athlete.name "Joe" %}
...
{% endifnotequal %}
{% if athlete_list|length > 1 %}
Team: {% for athlete in athlete_list %} ... {% endfor %}
{% else %}
Athlete: {{ athlete_list.0.name }}
{% endif %}
:ttag:`block` and :ttag:`extends`
Set up `template inheritance`_ (see below), a powerful way

View File

@ -0,0 +1,46 @@
import unittest
from django.template.smartif import IfParser, Literal
class SmartIfTests(unittest.TestCase):
def assertCalcEqual(self, expected, tokens):
self.assertEqual(expected, IfParser(tokens).parse().eval({}))
# We only test things here that are difficult to test elsewhere
# Many other tests are found in the main tests for builtin template tags
# Test parsing via the printed parse tree
def test_not(self):
var = IfParser(["not", False]).parse()
self.assertEqual("(not (literal False))", repr(var))
self.assert_(var.eval({}))
self.assertFalse(IfParser(["not", True]).parse().eval({}))
def test_or(self):
var = IfParser([True, "or", False]).parse()
self.assertEqual("(or (literal True) (literal False))", repr(var))
self.assert_(var.eval({}))
def test_in(self):
list_ = [1,2,3]
self.assertCalcEqual(True, [1, 'in', list_])
self.assertCalcEqual(False, [1, 'in', None])
self.assertCalcEqual(False, [None, 'in', list_])
def test_precedence(self):
# (False and False) or True == True <- we want this one, like Python
# False and (False or True) == False
self.assertCalcEqual(True, [False, 'and', False, 'or', True])
# True or (False and False) == True <- we want this one, like Python
# (True or False) and False == False
self.assertCalcEqual(True, [True, 'or', False, 'and', False])
# (1 or 1) == 2 -> False
# 1 or (1 == 2) -> True <- we want this one
self.assertCalcEqual(True, [1, 'or', 1, '==', 2])
self.assertCalcEqual(True, [True, '==', True, 'or', True, '==', False])
self.assertEqual("(or (and (== (literal 1) (literal 2)) (literal 3)) (literal 4))",
repr(IfParser([1, '==', 2, 'and', 3, 'or', 4]).parse()))

View File

@ -24,6 +24,7 @@ from context import context_tests
from custom import custom_filters
from parser import filter_parsing, variable_parsing
from unicode import unicode_tests
from smartif import *
try:
from loaders import *
@ -534,6 +535,27 @@ class Templates(unittest.TestCase):
'if-tag02': ("{% if foo %}yes{% else %}no{% endif %}", {"foo": False}, "no"),
'if-tag03': ("{% if foo %}yes{% else %}no{% endif %}", {}, "no"),
# Filters
'if-tag-filter01': ("{% if foo|length == 5 %}yes{% else %}no{% endif %}", {'foo': 'abcde'}, "yes"),
'if-tag-filter02': ("{% if foo|upper == 'ABC' %}yes{% else %}no{% endif %}", {}, "no"),
# Equality
'if-tag-eq01': ("{% if foo == bar %}yes{% else %}no{% endif %}", {}, "yes"),
'if-tag-eq02': ("{% if foo == bar %}yes{% else %}no{% endif %}", {'foo': 1}, "no"),
'if-tag-eq03': ("{% if foo == bar %}yes{% else %}no{% endif %}", {'foo': 1, 'bar': 1}, "yes"),
'if-tag-eq04': ("{% if foo == bar %}yes{% else %}no{% endif %}", {'foo': 1, 'bar': 2}, "no"),
'if-tag-eq05': ("{% if foo == '' %}yes{% else %}no{% endif %}", {}, "no"),
# Comparison
'if-tag-gt-01': ("{% if 2 > 1 %}yes{% else %}no{% endif %}", {}, "yes"),
'if-tag-gt-02': ("{% if 1 > 1 %}yes{% else %}no{% endif %}", {}, "no"),
'if-tag-gte-01': ("{% if 1 >= 1 %}yes{% else %}no{% endif %}", {}, "yes"),
'if-tag-gte-02': ("{% if 1 >= 2 %}yes{% else %}no{% endif %}", {}, "no"),
'if-tag-lt-01': ("{% if 1 < 2 %}yes{% else %}no{% endif %}", {}, "yes"),
'if-tag-lt-02': ("{% if 1 < 1 %}yes{% else %}no{% endif %}", {}, "no"),
'if-tag-lte-01': ("{% if 1 <= 1 %}yes{% else %}no{% endif %}", {}, "yes"),
'if-tag-lte-02': ("{% if 2 <= 1 %}yes{% else %}no{% endif %}", {}, "no"),
# AND
'if-tag-and01': ("{% if foo and bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': True}, 'yes'),
'if-tag-and02': ("{% if foo and bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': False}, 'no'),
@ -554,14 +576,13 @@ class Templates(unittest.TestCase):
'if-tag-or07': ("{% if foo or bar %}yes{% else %}no{% endif %}", {'foo': True}, 'yes'),
'if-tag-or08': ("{% if foo or bar %}yes{% else %}no{% endif %}", {'bar': True}, 'yes'),
# TODO: multiple ORs
# multiple ORs
'if-tag-or09': ("{% if foo or bar or baz %}yes{% else %}no{% endif %}", {'baz': True}, 'yes'),
# NOT
'if-tag-not01': ("{% if not foo %}no{% else %}yes{% endif %}", {'foo': True}, 'yes'),
'if-tag-not02': ("{% if not %}yes{% else %}no{% endif %}", {'foo': True}, 'no'),
'if-tag-not03': ("{% if not %}yes{% else %}no{% endif %}", {'not': True}, 'yes'),
'if-tag-not04': ("{% if not not %}no{% else %}yes{% endif %}", {'not': True}, 'yes'),
'if-tag-not05': ("{% if not not %}no{% else %}yes{% endif %}", {}, 'no'),
'if-tag-not02': ("{% if not not foo %}no{% else %}yes{% endif %}", {'foo': True}, 'no'),
# not03 to not05 removed, now TemplateSyntaxErrors
'if-tag-not06': ("{% if foo and not bar %}yes{% else %}no{% endif %}", {}, 'no'),
'if-tag-not07': ("{% if foo and not bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': True}, 'no'),
@ -599,12 +620,21 @@ class Templates(unittest.TestCase):
'if-tag-not34': ("{% if not foo or not bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': True}, 'yes'),
'if-tag-not35': ("{% if not foo or not bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': False}, 'yes'),
# AND and OR raises a TemplateSyntaxError
'if-tag-error01': ("{% if foo or bar and baz %}yes{% else %}no{% endif %}", {'foo': False, 'bar': False}, template.TemplateSyntaxError),
# Various syntax errors
'if-tag-error01': ("{% if %}yes{% endif %}", {}, template.TemplateSyntaxError),
'if-tag-error02': ("{% if foo and %}yes{% else %}no{% endif %}", {'foo': True}, template.TemplateSyntaxError),
'if-tag-error03': ("{% if foo or %}yes{% else %}no{% endif %}", {'foo': True}, template.TemplateSyntaxError),
'if-tag-error04': ("{% if not foo and %}yes{% else %}no{% endif %}", {'foo': True}, template.TemplateSyntaxError),
'if-tag-error05': ("{% if not foo or %}yes{% else %}no{% endif %}", {'foo': True}, template.TemplateSyntaxError),
'if-tag-error06': ("{% if abc def %}yes{% endif %}", {}, template.TemplateSyntaxError),
'if-tag-error07': ("{% if not %}yes{% endif %}", {}, template.TemplateSyntaxError),
'if-tag-error08': ("{% if and %}yes{% endif %}", {}, template.TemplateSyntaxError),
'if-tag-error09': ("{% if or %}yes{% endif %}", {}, template.TemplateSyntaxError),
'if-tag-error10': ("{% if == %}yes{% endif %}", {}, template.TemplateSyntaxError),
'if-tag-error11': ("{% if 1 == %}yes{% endif %}", {}, template.TemplateSyntaxError),
'if-tag-error12': ("{% if a not b %}yes{% endif %}", {}, template.TemplateSyntaxError),
# Additional, more precise parsing tests are in SmartIfTests
### IFCHANGED TAG #########################################################
'ifchanged01': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% endfor %}', {'num': (1,2,3)}, '123'),