Commit 2c2f5aee authored by Luke Plant's avatar Luke Plant
Browse files

Implemented 'smart if' template tag, allowing filters and various operators to...

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
parent 25020ddb
Loading
Loading
Loading
Loading
+39 −62
Original line number Diff line number Diff line
@@ -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):
        if self.var.eval(context):
            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)
            return self.nodelist_true.render(context)

    class LinkTypes:
        and_ = 0,
        or_ = 1

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 −0
Original line number Diff line number Diff line
"""
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)
+149 −11
Original line number Diff line number Diff line
@@ -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!
.. 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 %}

``!=`` 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 %}

Multiple uses of the same logical operator are fine, as long as you use the
same operator. For example, this is valid::
``<`` 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
+43 −0
Original line number Diff line number Diff line
@@ -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>
+8 −13
Original line number Diff line number Diff line
@@ -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
Loading