--- /dev/null
+"""\r
+A smarter {% if %} tag for django templates.\r
+\r
+While retaining current Django functionality, it also handles equality,\r
+greater than and less than operators. Some common case examples::\r
+\r
+ {% if articles|length >= 5 %}...{% endif %}\r
+ {% if "ifnotequal tag" != "beautiful" %}...{% endif %}\r
+"""\r
+import unittest\r
+from django import template\r
+\r
+\r
+register = template.Library()\r
+\r
+\r
+#==============================================================================\r
+# Calculation objects\r
+#==============================================================================\r
+\r
+class BaseCalc(object):\r
+ def __init__(self, var1, var2=None, negate=False):\r
+ self.var1 = var1\r
+ self.var2 = var2\r
+ self.negate = negate\r
+\r
+ def resolve(self, context):\r
+ try:\r
+ var1, var2 = self.resolve_vars(context)\r
+ outcome = self.calculate(var1, var2)\r
+ except:\r
+ outcome = False\r
+ if self.negate:\r
+ return not outcome\r
+ return outcome\r
+\r
+ def resolve_vars(self, context):\r
+ var2 = self.var2 and self.var2.resolve(context)\r
+ return self.var1.resolve(context), var2\r
+\r
+ def calculate(self, var1, var2):\r
+ raise NotImplementedError()\r
+\r
+\r
+class Or(BaseCalc):\r
+ def calculate(self, var1, var2):\r
+ return var1 or var2\r
+\r
+\r
+class And(BaseCalc):\r
+ def calculate(self, var1, var2):\r
+ return var1 and var2\r
+\r
+\r
+class Equals(BaseCalc):\r
+ def calculate(self, var1, var2):\r
+ return var1 == var2\r
+\r
+\r
+class Greater(BaseCalc):\r
+ def calculate(self, var1, var2):\r
+ return var1 > var2\r
+\r
+\r
+class GreaterOrEqual(BaseCalc):\r
+ def calculate(self, var1, var2):\r
+ return var1 >= var2\r
+\r
+\r
+class In(BaseCalc):\r
+ def calculate(self, var1, var2):\r
+ return var1 in var2\r
+\r
+\r
+#==============================================================================\r
+# Tests\r
+#==============================================================================\r
+\r
+class TestVar(object):\r
+ """\r
+ A basic self-resolvable object similar to a Django template variable. Used\r
+ to assist with tests.\r
+ """\r
+ def __init__(self, value):\r
+ self.value = value\r
+\r
+ def resolve(self, context):\r
+ return self.value\r
+\r
+\r
+class SmartIfTests(unittest.TestCase):\r
+ def setUp(self):\r
+ self.true = TestVar(True)\r
+ self.false = TestVar(False)\r
+ self.high = TestVar(9000)\r
+ self.low = TestVar(1)\r
+\r
+ def assertCalc(self, calc, context=None):\r
+ """\r
+ Test a calculation is True, also checking the inverse "negate" case.\r
+ """\r
+ context = context or {}\r
+ self.assert_(calc.resolve(context))\r
+ calc.negate = not calc.negate\r
+ self.assertFalse(calc.resolve(context))\r
+\r
+ def assertCalcFalse(self, calc, context=None):\r
+ """\r
+ Test a calculation is False, also checking the inverse "negate" case.\r
+ """\r
+ context = context or {}\r
+ self.assertFalse(calc.resolve(context))\r
+ calc.negate = not calc.negate\r
+ self.assert_(calc.resolve(context))\r
+\r
+ def test_or(self):\r
+ self.assertCalc(Or(self.true))\r
+ self.assertCalcFalse(Or(self.false))\r
+ self.assertCalc(Or(self.true, self.true))\r
+ self.assertCalc(Or(self.true, self.false))\r
+ self.assertCalc(Or(self.false, self.true))\r
+ self.assertCalcFalse(Or(self.false, self.false))\r
+\r
+ def test_and(self):\r
+ self.assertCalc(And(self.true, self.true))\r
+ self.assertCalcFalse(And(self.true, self.false))\r
+ self.assertCalcFalse(And(self.false, self.true))\r
+ self.assertCalcFalse(And(self.false, self.false))\r
+\r
+ def test_equals(self):\r
+ self.assertCalc(Equals(self.low, self.low))\r
+ self.assertCalcFalse(Equals(self.low, self.high))\r
+\r
+ def test_greater(self):\r
+ self.assertCalc(Greater(self.high, self.low))\r
+ self.assertCalcFalse(Greater(self.low, self.low))\r
+ self.assertCalcFalse(Greater(self.low, self.high))\r
+\r
+ def test_greater_or_equal(self):\r
+ self.assertCalc(GreaterOrEqual(self.high, self.low))\r
+ self.assertCalc(GreaterOrEqual(self.low, self.low))\r
+ self.assertCalcFalse(GreaterOrEqual(self.low, self.high))\r
+\r
+ def test_in(self):\r
+ list_ = TestVar([1,2,3])\r
+ invalid_list = TestVar(None)\r
+ self.assertCalc(In(self.low, list_))\r
+ self.assertCalcFalse(In(self.low, invalid_list))\r
+\r
+ def test_parse_bits(self):\r
+ var = IfParser([True]).parse()\r
+ self.assert_(var.resolve({}))\r
+ var = IfParser([False]).parse()\r
+ self.assertFalse(var.resolve({}))\r
+\r
+ var = IfParser([False, 'or', True]).parse()\r
+ self.assert_(var.resolve({}))\r
+\r
+ var = IfParser([False, 'and', True]).parse()\r
+ self.assertFalse(var.resolve({}))\r
+\r
+ var = IfParser(['not', False, 'and', 'not', False]).parse()\r
+ self.assert_(var.resolve({}))\r
+\r
+ var = IfParser(['not', 'not', True]).parse()\r
+ self.assert_(var.resolve({}))\r
+\r
+ var = IfParser([1, '=', 1]).parse()\r
+ self.assert_(var.resolve({}))\r
+\r
+ var = IfParser([1, 'not', '=', 1]).parse()\r
+ self.assertFalse(var.resolve({}))\r
+\r
+ var = IfParser([1, 'not', 'not', '=', 1]).parse()\r
+ self.assert_(var.resolve({}))\r
+\r
+ var = IfParser([1, '!=', 1]).parse()\r
+ self.assertFalse(var.resolve({}))\r
+\r
+ var = IfParser([3, '>', 2]).parse()\r
+ self.assert_(var.resolve({}))\r
+\r
+ var = IfParser([1, '<', 2]).parse()\r
+ self.assert_(var.resolve({}))\r
+\r
+ var = IfParser([2, 'not', 'in', [2, 3]]).parse()\r
+ self.assertFalse(var.resolve({}))\r
+\r
+ var = IfParser([1, 'or', 1, '=', 2]).parse()\r
+ self.assert_(var.resolve({}))\r
+\r
+ def test_boolean(self):\r
+ var = IfParser([True, 'and', True, 'and', True]).parse()\r
+ self.assert_(var.resolve({}))\r
+ var = IfParser([False, 'or', False, 'or', True]).parse()\r
+ self.assert_(var.resolve({}))\r
+ var = IfParser([True, 'and', False, 'or', True]).parse()\r
+ self.assert_(var.resolve({}))\r
+ var = IfParser([False, 'or', True, 'and', True]).parse()\r
+ self.assert_(var.resolve({}))\r
+\r
+ var = IfParser([True, 'and', True, 'and', False]).parse()\r
+ self.assertFalse(var.resolve({}))\r
+ var = IfParser([False, 'or', False, 'or', False]).parse()\r
+ self.assertFalse(var.resolve({}))\r
+ var = IfParser([False, 'or', True, 'and', False]).parse()\r
+ self.assertFalse(var.resolve({}))\r
+ var = IfParser([False, 'and', True, 'or', False]).parse()\r
+ self.assertFalse(var.resolve({}))\r
+\r
+ def test_invalid(self):\r
+ self.assertRaises(ValueError, IfParser(['not']).parse)\r
+ self.assertRaises(ValueError, IfParser(['==']).parse)\r
+ self.assertRaises(ValueError, IfParser([1, 'in']).parse)\r
+ self.assertRaises(ValueError, IfParser([1, '>', 'in']).parse)\r
+ self.assertRaises(ValueError, IfParser([1, '==', 'not', 'not']).parse)\r
+ self.assertRaises(ValueError, IfParser([1, 2]).parse)\r
+\r
+\r
+OPERATORS = {\r
+ '=': (Equals, True),\r
+ '==': (Equals, True),\r
+ '!=': (Equals, False),\r
+ '>': (Greater, True),\r
+ '>=': (GreaterOrEqual, True),\r
+ '<=': (Greater, False),\r
+ '<': (GreaterOrEqual, False),\r
+ 'or': (Or, True),\r
+ 'and': (And, True),\r
+ 'in': (In, True),\r
+}\r
+BOOL_OPERATORS = ('or', 'and')\r
+\r
+\r
+class IfParser(object):\r
+ error_class = ValueError\r
+\r
+ def __init__(self, tokens):\r
+ self.tokens = tokens\r
+\r
+ def _get_tokens(self):\r
+ return self._tokens\r
+\r
+ def _set_tokens(self, tokens):\r
+ self._tokens = tokens\r
+ self.len = len(tokens)\r
+ self.pos = 0\r
+\r
+ tokens = property(_get_tokens, _set_tokens)\r
+\r
+ def parse(self):\r
+ if self.at_end():\r
+ raise self.error_class('No variables provided.')\r
+ var1 = self.get_bool_var()\r
+ while not self.at_end():\r
+ op, negate = self.get_operator()\r
+ var2 = self.get_bool_var()\r
+ var1 = op(var1, var2, negate=negate)\r
+ return var1\r
+\r
+ def get_token(self, eof_message=None, lookahead=False):\r
+ negate = True\r
+ token = None\r
+ pos = self.pos\r
+ while token is None or token == 'not':\r
+ if pos >= self.len:\r
+ if eof_message is None:\r
+ raise self.error_class()\r
+ raise self.error_class(eof_message)\r
+ token = self.tokens[pos]\r
+ negate = not negate\r
+ pos += 1\r
+ if not lookahead:\r
+ self.pos = pos\r
+ return token, negate\r
+\r
+ def at_end(self):\r
+ return self.pos >= self.len\r
+\r
+ def create_var(self, value):\r
+ return TestVar(value)\r
+\r
+ def get_bool_var(self):\r
+ """\r
+ Returns either a variable by itself or a non-boolean operation (such as\r
+ ``x == 0`` or ``x < 0``).\r
+\r
+ This is needed to keep correct precedence for boolean operations (i.e.\r
+ ``x or x == 0`` should be ``x or (x == 0)``, not ``(x or x) == 0``).\r
+ """\r
+ var = self.get_var()\r
+ if not self.at_end():\r
+ op_token = self.get_token(lookahead=True)[0]\r
+ if isinstance(op_token, basestring) and (op_token not in\r
+ BOOL_OPERATORS):\r
+ op, negate = self.get_operator()\r
+ return op(var, self.get_var(), negate=negate)\r
+ return var\r
+\r
+ def get_var(self):\r
+ token, negate = self.get_token('Reached end of statement, still '\r
+ 'expecting a variable.')\r
+ if isinstance(token, basestring) and token in OPERATORS:\r
+ raise self.error_class('Expected variable, got operator (%s).' %\r
+ token)\r
+ var = self.create_var(token)\r
+ if negate:\r
+ return Or(var, negate=True)\r
+ return var\r
+\r
+ def get_operator(self):\r
+ token, negate = self.get_token('Reached end of statement, still '\r
+ 'expecting an operator.')\r
+ if not isinstance(token, basestring) or token not in OPERATORS:\r
+ raise self.error_class('%s is not a valid operator.' % token)\r
+ if self.at_end():\r
+ raise self.error_class('No variable provided after "%s".' % token)\r
+ op, true = OPERATORS[token]\r
+ if not true:\r
+ negate = not negate\r
+ return op, negate\r
+\r
+\r
+#==============================================================================\r
+# Actual templatetag code.\r
+#==============================================================================\r
+\r
+class TemplateIfParser(IfParser):\r
+ error_class = template.TemplateSyntaxError\r
+\r
+ def __init__(self, parser, *args, **kwargs):\r
+ self.template_parser = parser\r
+ return super(TemplateIfParser, self).__init__(*args, **kwargs)\r
+\r
+ def create_var(self, value):\r
+ return self.template_parser.compile_filter(value)\r
+\r
+\r
+class SmartIfNode(template.Node):\r
+ def __init__(self, var, nodelist_true, nodelist_false=None):\r
+ self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false\r
+ self.var = var\r
+\r
+ def render(self, context):\r
+ if self.var.resolve(context):\r
+ return self.nodelist_true.render(context)\r
+ if self.nodelist_false:\r
+ return self.nodelist_false.render(context)\r
+ return ''\r
+\r
+ def __repr__(self):\r
+ return "<Smart If node>"\r
+\r
+ def __iter__(self):\r
+ for node in self.nodelist_true:\r
+ yield node\r
+ if self.nodelist_false:\r
+ for node in self.nodelist_false:\r
+ yield node\r
+\r
+ def get_nodes_by_type(self, nodetype):\r
+ nodes = []\r
+ if isinstance(self, nodetype):\r
+ nodes.append(self)\r
+ nodes.extend(self.nodelist_true.get_nodes_by_type(nodetype))\r
+ if self.nodelist_false:\r
+ nodes.extend(self.nodelist_false.get_nodes_by_type(nodetype))\r
+ return nodes\r
+\r
+\r
+@register.tag('if')\r
+def smart_if(parser, token):\r
+ """\r
+ A smarter {% if %} tag for django templates.\r
+\r
+ While retaining current Django functionality, it also handles equality,\r
+ greater than and less than operators. Some common case examples::\r
+\r
+ {% if articles|length >= 5 %}...{% endif %}\r
+ {% if "ifnotequal tag" != "beautiful" %}...{% endif %}\r
+\r
+ Arguments and operators _must_ have a space between them, so\r
+ ``{% if 1>2 %}`` is not a valid smart if tag.\r
+\r
+ All supported operators are: ``or``, ``and``, ``in``, ``=`` (or ``==``),\r
+ ``!=``, ``>``, ``>=``, ``<`` and ``<=``.\r
+ """\r
+ bits = token.split_contents()[1:]\r
+ var = TemplateIfParser(parser, bits).parse()\r
+ nodelist_true = parser.parse(('else', 'endif'))\r
+ token = parser.next_token()\r
+ if token.contents == 'else':\r
+ nodelist_false = parser.parse(('endif',))\r
+ parser.delete_first_token()\r
+ else:\r
+ nodelist_false = None\r
+ return SmartIfNode(var, nodelist_true, nodelist_false)\r
+\r
+\r
+if __name__ == '__main__':\r
+ unittest.main()\r