2
A smarter {% if %} tag for django templates.
4
While retaining current Django functionality, it also handles equality,
5
greater than and less than operators. Some common case examples::
7
{% if articles|length >= 5 %}...{% endif %}
8
{% if "ifnotequal tag" != "beautiful" %}...{% endif %}
11
from django import template
14
register = template.Library()
17
#==============================================================================
19
#==============================================================================
21
class BaseCalc(object):
22
def __init__(self, var1, var2=None, negate=False):
27
def resolve(self, context):
29
var1, var2 = self.resolve_vars(context)
30
outcome = self.calculate(var1, var2)
37
def resolve_vars(self, context):
38
var2 = self.var2 and self.var2.resolve(context)
39
return self.var1.resolve(context), var2
41
def calculate(self, var1, var2):
42
raise NotImplementedError()
46
def calculate(self, var1, var2):
51
def calculate(self, var1, var2):
55
class Equals(BaseCalc):
56
def calculate(self, var1, var2):
60
class Greater(BaseCalc):
61
def calculate(self, var1, var2):
65
class GreaterOrEqual(BaseCalc):
66
def calculate(self, var1, var2):
71
def calculate(self, var1, var2):
75
#==============================================================================
77
#==============================================================================
79
class TestVar(object):
81
A basic self-resolvable object similar to a Django template variable. Used
84
def __init__(self, value):
87
def resolve(self, context):
91
class SmartIfTests(unittest.TestCase):
93
self.true = TestVar(True)
94
self.false = TestVar(False)
95
self.high = TestVar(9000)
98
def assertCalc(self, calc, context=None):
100
Test a calculation is True, also checking the inverse "negate" case.
102
context = context or {}
103
self.assert_(calc.resolve(context))
104
calc.negate = not calc.negate
105
self.assertFalse(calc.resolve(context))
107
def assertCalcFalse(self, calc, context=None):
109
Test a calculation is False, also checking the inverse "negate" case.
111
context = context or {}
112
self.assertFalse(calc.resolve(context))
113
calc.negate = not calc.negate
114
self.assert_(calc.resolve(context))
117
self.assertCalc(Or(self.true))
118
self.assertCalcFalse(Or(self.false))
119
self.assertCalc(Or(self.true, self.true))
120
self.assertCalc(Or(self.true, self.false))
121
self.assertCalc(Or(self.false, self.true))
122
self.assertCalcFalse(Or(self.false, self.false))
125
self.assertCalc(And(self.true, self.true))
126
self.assertCalcFalse(And(self.true, self.false))
127
self.assertCalcFalse(And(self.false, self.true))
128
self.assertCalcFalse(And(self.false, self.false))
130
def test_equals(self):
131
self.assertCalc(Equals(self.low, self.low))
132
self.assertCalcFalse(Equals(self.low, self.high))
134
def test_greater(self):
135
self.assertCalc(Greater(self.high, self.low))
136
self.assertCalcFalse(Greater(self.low, self.low))
137
self.assertCalcFalse(Greater(self.low, self.high))
139
def test_greater_or_equal(self):
140
self.assertCalc(GreaterOrEqual(self.high, self.low))
141
self.assertCalc(GreaterOrEqual(self.low, self.low))
142
self.assertCalcFalse(GreaterOrEqual(self.low, self.high))
145
list_ = TestVar([1,2,3])
146
invalid_list = TestVar(None)
147
self.assertCalc(In(self.low, list_))
148
self.assertCalcFalse(In(self.low, invalid_list))
150
def test_parse_bits(self):
151
var = IfParser([True]).parse()
152
self.assert_(var.resolve({}))
153
var = IfParser([False]).parse()
154
self.assertFalse(var.resolve({}))
156
var = IfParser([False, 'or', True]).parse()
157
self.assert_(var.resolve({}))
159
var = IfParser([False, 'and', True]).parse()
160
self.assertFalse(var.resolve({}))
162
var = IfParser(['not', False, 'and', 'not', False]).parse()
163
self.assert_(var.resolve({}))
165
var = IfParser(['not', 'not', True]).parse()
166
self.assert_(var.resolve({}))
168
var = IfParser([1, '=', 1]).parse()
169
self.assert_(var.resolve({}))
171
var = IfParser([1, 'not', '=', 1]).parse()
172
self.assertFalse(var.resolve({}))
174
var = IfParser([1, 'not', 'not', '=', 1]).parse()
175
self.assert_(var.resolve({}))
177
var = IfParser([1, '!=', 1]).parse()
178
self.assertFalse(var.resolve({}))
180
var = IfParser([3, '>', 2]).parse()
181
self.assert_(var.resolve({}))
183
var = IfParser([1, '<', 2]).parse()
184
self.assert_(var.resolve({}))
186
var = IfParser([2, 'not', 'in', [2, 3]]).parse()
187
self.assertFalse(var.resolve({}))
189
var = IfParser([1, 'or', 1, '=', 2]).parse()
190
self.assert_(var.resolve({}))
192
def test_boolean(self):
193
var = IfParser([True, 'and', True, 'and', True]).parse()
194
self.assert_(var.resolve({}))
195
var = IfParser([False, 'or', False, 'or', True]).parse()
196
self.assert_(var.resolve({}))
197
var = IfParser([True, 'and', False, 'or', True]).parse()
198
self.assert_(var.resolve({}))
199
var = IfParser([False, 'or', True, 'and', True]).parse()
200
self.assert_(var.resolve({}))
202
var = IfParser([True, 'and', True, 'and', False]).parse()
203
self.assertFalse(var.resolve({}))
204
var = IfParser([False, 'or', False, 'or', False]).parse()
205
self.assertFalse(var.resolve({}))
206
var = IfParser([False, 'or', True, 'and', False]).parse()
207
self.assertFalse(var.resolve({}))
208
var = IfParser([False, 'and', True, 'or', False]).parse()
209
self.assertFalse(var.resolve({}))
211
def test_invalid(self):
212
self.assertRaises(ValueError, IfParser(['not']).parse)
213
self.assertRaises(ValueError, IfParser(['==']).parse)
214
self.assertRaises(ValueError, IfParser([1, 'in']).parse)
215
self.assertRaises(ValueError, IfParser([1, '>', 'in']).parse)
216
self.assertRaises(ValueError, IfParser([1, '==', 'not', 'not']).parse)
217
self.assertRaises(ValueError, IfParser([1, 2]).parse)
222
'==': (Equals, True),
223
'!=': (Equals, False),
224
'>': (Greater, True),
225
'>=': (GreaterOrEqual, True),
226
'<=': (Greater, False),
227
'<': (GreaterOrEqual, False),
232
BOOL_OPERATORS = ('or', 'and')
235
class IfParser(object):
236
error_class = ValueError
238
def __init__(self, tokens):
241
def _get_tokens(self):
244
def _set_tokens(self, tokens):
245
self._tokens = tokens
246
self.len = len(tokens)
249
tokens = property(_get_tokens, _set_tokens)
253
raise self.error_class('No variables provided.')
254
var1 = self.get_bool_var()
255
while not self.at_end():
256
op, negate = self.get_operator()
257
var2 = self.get_bool_var()
258
var1 = op(var1, var2, negate=negate)
261
def get_token(self, eof_message=None, lookahead=False):
265
while token is None or token == 'not':
267
if eof_message is None:
268
raise self.error_class()
269
raise self.error_class(eof_message)
270
token = self.tokens[pos]
278
return self.pos >= self.len
280
def create_var(self, value):
281
return TestVar(value)
283
def get_bool_var(self):
285
Returns either a variable by itself or a non-boolean operation (such as
286
``x == 0`` or ``x < 0``).
288
This is needed to keep correct precedence for boolean operations (i.e.
289
``x or x == 0`` should be ``x or (x == 0)``, not ``(x or x) == 0``).
292
if not self.at_end():
293
op_token = self.get_token(lookahead=True)[0]
294
if isinstance(op_token, basestring) and (op_token not in
296
op, negate = self.get_operator()
297
return op(var, self.get_var(), negate=negate)
301
token, negate = self.get_token('Reached end of statement, still '
302
'expecting a variable.')
303
if isinstance(token, basestring) and token in OPERATORS:
304
raise self.error_class('Expected variable, got operator (%s).' %
306
var = self.create_var(token)
308
return Or(var, negate=True)
311
def get_operator(self):
312
token, negate = self.get_token('Reached end of statement, still '
313
'expecting an operator.')
314
if not isinstance(token, basestring) or token not in OPERATORS:
315
raise self.error_class('%s is not a valid operator.' % token)
317
raise self.error_class('No variable provided after "%s".' % token)
318
op, true = OPERATORS[token]
324
#==============================================================================
325
# Actual templatetag code.
326
#==============================================================================
328
class TemplateIfParser(IfParser):
329
error_class = template.TemplateSyntaxError
331
def __init__(self, parser, *args, **kwargs):
332
self.template_parser = parser
333
return super(TemplateIfParser, self).__init__(*args, **kwargs)
335
def create_var(self, value):
336
return self.template_parser.compile_filter(value)
339
class SmartIfNode(template.Node):
340
def __init__(self, var, nodelist_true, nodelist_false=None):
341
self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false
344
def render(self, context):
345
if self.var.resolve(context):
346
return self.nodelist_true.render(context)
347
if self.nodelist_false:
348
return self.nodelist_false.render(context)
352
return "<Smart If node>"
355
for node in self.nodelist_true:
357
if self.nodelist_false:
358
for node in self.nodelist_false:
361
def get_nodes_by_type(self, nodetype):
363
if isinstance(self, nodetype):
365
nodes.extend(self.nodelist_true.get_nodes_by_type(nodetype))
366
if self.nodelist_false:
367
nodes.extend(self.nodelist_false.get_nodes_by_type(nodetype))
372
def smart_if(parser, token):
374
A smarter {% if %} tag for django templates.
376
While retaining current Django functionality, it also handles equality,
377
greater than and less than operators. Some common case examples::
379
{% if articles|length >= 5 %}...{% endif %}
380
{% if "ifnotequal tag" != "beautiful" %}...{% endif %}
382
Arguments and operators _must_ have a space between them, so
383
``{% if 1>2 %}`` is not a valid smart if tag.
385
All supported operators are: ``or``, ``and``, ``in``, ``=`` (or ``==``),
386
``!=``, ``>``, ``>=``, ``<`` and ``<=``.
388
bits = token.split_contents()[1:]
389
var = TemplateIfParser(parser, bits).parse()
390
nodelist_true = parser.parse(('else', 'endif'))
391
token = parser.next_token()
392
if token.contents == 'else':
393
nodelist_false = parser.parse(('endif',))
394
parser.delete_first_token()
396
nodelist_false = None
397
return SmartIfNode(var, nodelist_true, nodelist_false)
400
if __name__ == '__main__':