Skip to content

Commit 2c2f5ae

Browse files
committed
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
1 parent 25020dd commit 2c2f5ae

File tree

7 files changed

+514
-93
lines changed

7 files changed

+514
-93
lines changed

django/template/defaulttags.py

Lines changed: 39 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from django.template import Node, NodeList, Template, Context, Variable
1212
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
1313
from django.template import get_library, Library, InvalidTemplateLibrary
14+
from django.template.smartif import IfParser, Literal
1415
from django.conf import settings
1516
from django.utils.encoding import smart_str, smart_unicode
1617
from django.utils.itercompat import groupby
@@ -227,10 +228,9 @@ def render(self, context):
227228
return self.nodelist_false.render(context)
228229

229230
class IfNode(Node):
230-
def __init__(self, bool_exprs, nodelist_true, nodelist_false, link_type):
231-
self.bool_exprs = bool_exprs
231+
def __init__(self, var, nodelist_true, nodelist_false=None):
232232
self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false
233-
self.link_type = link_type
233+
self.var = var
234234

235235
def __repr__(self):
236236
return "<If node>"
@@ -250,28 +250,10 @@ def get_nodes_by_type(self, nodetype):
250250
return nodes
251251

252252
def render(self, context):
253-
if self.link_type == IfNode.LinkTypes.or_:
254-
for ifnot, bool_expr in self.bool_exprs:
255-
try:
256-
value = bool_expr.resolve(context, True)
257-
except VariableDoesNotExist:
258-
value = None
259-
if (value and not ifnot) or (ifnot and not value):
260-
return self.nodelist_true.render(context)
261-
return self.nodelist_false.render(context)
262-
else:
263-
for ifnot, bool_expr in self.bool_exprs:
264-
try:
265-
value = bool_expr.resolve(context, True)
266-
except VariableDoesNotExist:
267-
value = None
268-
if not ((value and not ifnot) or (ifnot and not value)):
269-
return self.nodelist_false.render(context)
253+
if self.var.eval(context):
270254
return self.nodelist_true.render(context)
271-
272-
class LinkTypes:
273-
and_ = 0,
274-
or_ = 1
255+
else:
256+
return self.nodelist_false.render(context)
275257

276258
class RegroupNode(Node):
277259
def __init__(self, target, expression, var_name):
@@ -761,6 +743,27 @@ def ifnotequal(parser, token):
761743
return do_ifequal(parser, token, True)
762744
ifnotequal = register.tag(ifnotequal)
763745

746+
class TemplateLiteral(Literal):
747+
def __init__(self, value, text):
748+
self.value = value
749+
self.text = text # for better error messages
750+
751+
def display(self):
752+
return self.text
753+
754+
def eval(self, context):
755+
return self.value.resolve(context, ignore_failures=True)
756+
757+
class TemplateIfParser(IfParser):
758+
error_class = TemplateSyntaxError
759+
760+
def __init__(self, parser, *args, **kwargs):
761+
self.template_parser = parser
762+
return super(TemplateIfParser, self).__init__(*args, **kwargs)
763+
764+
def create_var(self, value):
765+
return TemplateLiteral(self.template_parser.compile_filter(value), value)
766+
764767
#@register.tag(name="if")
765768
def do_if(parser, token):
766769
"""
@@ -805,55 +808,29 @@ def do_if(parser, token):
805808
There are some athletes and absolutely no coaches.
806809
{% endif %}
807810
808-
``if`` tags do not allow ``and`` and ``or`` clauses with the same tag,
809-
because the order of logic would be ambigous. For example, this is
810-
invalid::
811+
Comparison operators are also available, and the use of filters is also
812+
allowed, for example:
811813
812-
{% if athlete_list and coach_list or cheerleader_list %}
814+
{% if articles|length >= 5 %}...{% endif %}
813815
814-
If you need to combine ``and`` and ``or`` to do advanced logic, just use
815-
nested if tags. For example::
816+
Arguments and operators _must_ have a space between them, so
817+
``{% if 1>2 %}`` is not a valid if tag.
816818
817-
{% if athlete_list %}
818-
{% if coach_list or cheerleader_list %}
819-
We have athletes, and either coaches or cheerleaders!
820-
{% endif %}
821-
{% endif %}
819+
All supported operators are: ``or``, ``and``, ``in``, ``==`` (or ``=``),
820+
``!=``, ``>``, ``>=``, ``<`` and ``<=``.
821+
822+
Operator precedence follows Python.
822823
"""
823-
bits = token.contents.split()
824-
del bits[0]
825-
if not bits:
826-
raise TemplateSyntaxError("'if' statement requires at least one argument")
827-
# Bits now looks something like this: ['a', 'or', 'not', 'b', 'or', 'c.d']
828-
bitstr = ' '.join(bits)
829-
boolpairs = bitstr.split(' and ')
830-
boolvars = []
831-
if len(boolpairs) == 1:
832-
link_type = IfNode.LinkTypes.or_
833-
boolpairs = bitstr.split(' or ')
834-
else:
835-
link_type = IfNode.LinkTypes.and_
836-
if ' or ' in bitstr:
837-
raise TemplateSyntaxError, "'if' tags can't mix 'and' and 'or'"
838-
for boolpair in boolpairs:
839-
if ' ' in boolpair:
840-
try:
841-
not_, boolvar = boolpair.split()
842-
except ValueError:
843-
raise TemplateSyntaxError, "'if' statement improperly formatted"
844-
if not_ != 'not':
845-
raise TemplateSyntaxError, "Expected 'not' in if statement"
846-
boolvars.append((True, parser.compile_filter(boolvar)))
847-
else:
848-
boolvars.append((False, parser.compile_filter(boolpair)))
824+
bits = token.split_contents()[1:]
825+
var = TemplateIfParser(parser, bits).parse()
849826
nodelist_true = parser.parse(('else', 'endif'))
850827
token = parser.next_token()
851828
if token.contents == 'else':
852829
nodelist_false = parser.parse(('endif',))
853830
parser.delete_first_token()
854831
else:
855832
nodelist_false = NodeList()
856-
return IfNode(boolvars, nodelist_true, nodelist_false, link_type)
833+
return IfNode(var, nodelist_true, nodelist_false)
857834
do_if = register.tag("if", do_if)
858835

859836
#@register.tag

django/template/smartif.py

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
"""
2+
Parser and utilities for the smart 'if' tag
3+
"""
4+
import operator
5+
6+
# Using a simple top down parser, as described here:
7+
# http://effbot.org/zone/simple-top-down-parsing.htm.
8+
# 'led' = left denotation
9+
# 'nud' = null denotation
10+
# 'bp' = binding power (left = lbp, right = rbp)
11+
12+
class TokenBase(object):
13+
"""
14+
Base class for operators and literals, mainly for debugging and for throwing
15+
syntax errors.
16+
"""
17+
id = None # node/token type name
18+
value = None # used by literals
19+
first = second = None # used by tree nodes
20+
21+
def nud(self, parser):
22+
# Null denotation - called in prefix context
23+
raise parser.error_class(
24+
"Not expecting '%s' in this position in if tag." % self.id
25+
)
26+
27+
def led(self, left, parser):
28+
# Left denotation - called in infix context
29+
raise parser.error_class(
30+
"Not expecting '%s' as infix operator in if tag." % self.id
31+
)
32+
33+
def display(self):
34+
"""
35+
Returns what to display in error messages for this node
36+
"""
37+
return self.id
38+
39+
def __repr__(self):
40+
out = [str(x) for x in [self.id, self.first, self.second] if x is not None]
41+
return "(" + " ".join(out) + ")"
42+
43+
44+
def infix(bp, func):
45+
"""
46+
Creates an infix operator, given a binding power and a function that
47+
evaluates the node
48+
"""
49+
class Operator(TokenBase):
50+
lbp = bp
51+
52+
def led(self, left, parser):
53+
self.first = left
54+
self.second = parser.expression(bp)
55+
return self
56+
57+
def eval(self, context):
58+
try:
59+
return func(self.first.eval(context), self.second.eval(context))
60+
except Exception:
61+
# Templates shouldn't throw exceptions when rendering. We are
62+
# most likely to get exceptions for things like {% if foo in bar
63+
# %} where 'bar' does not support 'in', so default to False
64+
return False
65+
66+
return Operator
67+
68+
69+
def prefix(bp, func):
70+
"""
71+
Creates a prefix operator, given a binding power and a function that
72+
evaluates the node.
73+
"""
74+
class Operator(TokenBase):
75+
lbp = bp
76+
77+
def nud(self, parser):
78+
self.first = parser.expression(bp)
79+
self.second = None
80+
return self
81+
82+
def eval(self, context):
83+
try:
84+
return func(self.first.eval(context))
85+
except Exception:
86+
return False
87+
88+
return Operator
89+
90+
91+
# Operator precedence follows Python.
92+
# NB - we can get slightly more accurate syntax error messages by not using the
93+
# same object for '==' and '='.
94+
95+
OPERATORS = {
96+
'or': infix(6, lambda x, y: x or y),
97+
'and': infix(7, lambda x, y: x and y),
98+
'not': prefix(8, operator.not_),
99+
'in': infix(9, lambda x, y: x in y),
100+
'=': infix(10, operator.eq),
101+
'==': infix(10, operator.eq),
102+
'!=': infix(10, operator.ne),
103+
'>': infix(10, operator.gt),
104+
'>=': infix(10, operator.ge),
105+
'<': infix(10, operator.lt),
106+
'<=': infix(10, operator.le),
107+
}
108+
109+
# Assign 'id' to each:
110+
for key, op in OPERATORS.items():
111+
op.id = key
112+
113+
114+
class Literal(TokenBase):
115+
"""
116+
A basic self-resolvable object similar to a Django template variable.
117+
"""
118+
# IfParser uses Literal in create_var, but TemplateIfParser overrides
119+
# create_var so that a proper implementation that actually resolves
120+
# variables, filters etc is used.
121+
id = "literal"
122+
lbp = 0
123+
124+
def __init__(self, value):
125+
self.value = value
126+
127+
def display(self):
128+
return repr(self.value)
129+
130+
def nud(self, parser):
131+
return self
132+
133+
def eval(self, context):
134+
return self.value
135+
136+
def __repr__(self):
137+
return "(%s %r)" % (self.id, self.value)
138+
139+
140+
class EndToken(TokenBase):
141+
lbp = 0
142+
143+
def nud(self, parser):
144+
raise parser.error_class("Unexpected end of expression in if tag.")
145+
146+
EndToken = EndToken()
147+
148+
149+
class IfParser(object):
150+
error_class = ValueError
151+
152+
def __init__(self, tokens):
153+
self.tokens = map(self.translate_tokens, tokens)
154+
self.pos = 0
155+
self.current_token = self.next()
156+
157+
def translate_tokens(self, token):
158+
try:
159+
op = OPERATORS[token]
160+
except (KeyError, TypeError):
161+
return self.create_var(token)
162+
else:
163+
return op()
164+
165+
def next(self):
166+
if self.pos >= len(self.tokens):
167+
return EndToken
168+
else:
169+
retval = self.tokens[self.pos]
170+
self.pos += 1
171+
return retval
172+
173+
def parse(self):
174+
retval = self.expression()
175+
# Check that we have exhausted all the tokens
176+
if self.current_token is not EndToken:
177+
raise self.error_class("Unused '%s' at end of if expression." %
178+
self.current_token.display())
179+
return retval
180+
181+
def expression(self, rbp=0):
182+
t = self.current_token
183+
self.current_token = self.next()
184+
left = t.nud(self)
185+
while rbp < self.current_token.lbp:
186+
t = self.current_token
187+
self.current_token = self.next()
188+
left = t.led(left, self)
189+
return left
190+
191+
def create_var(self, value):
192+
return Literal(value)

0 commit comments

Comments
 (0)