from builtins import object
from django import template
from collections import OrderedDict
register = template.Library()
# ==========================
# -*- coding: utf-8 -*-
'''
A smarter {% if %} tag for django templates.
While retaining current Django functionality, it also handles equality,
greater than and less than operators. Some common case examples::
{% if articles|length >= 5 %}...{% endif %}
{% if "ifnotequal tag" != "beautiful" %}...{% endif %}
'''
import unittest
from django import template
register = template.Library()
# ===============================================================================
# Calculation objects
# ===============================================================================
[docs]class BaseCalc(object):
def __init__(self, var1, var2=None, negate=False):
self.var1 = var1
self.var2 = var2
self.negate = negate
[docs] def resolve(self, context):
try:
var1, var2 = self.resolve_vars(context)
outcome = self.calculate(var1, var2)
except:
outcome = False
if self.negate:
return not outcome
return outcome
[docs] def resolve_vars(self, context):
var2 = self.var2 and self.var2.resolve(context)
return self.var1.resolve(context), var2
[docs] def calculate(self, var1, var2):
raise NotImplementedError()
[docs]class Or(BaseCalc):
[docs] def calculate(self, var1, var2):
return var1 or var2
[docs]class And(BaseCalc):
[docs] def calculate(self, var1, var2):
return var1 and var2
[docs]class Equals(BaseCalc):
[docs] def calculate(self, var1, var2):
return var1 == var2
[docs]class Greater(BaseCalc):
[docs] def calculate(self, var1, var2):
return var1 > var2
[docs]class GreaterOrEqual(BaseCalc):
[docs] def calculate(self, var1, var2):
return var1 >= var2
[docs]class In(BaseCalc):
[docs] def calculate(self, var1, var2):
return var1 in var2
# ===============================================================================
# Tests
# ===============================================================================
[docs]class TestVar(object):
"""
A basic self-resolvable object similar to a Django template variable. Used
to assist with tests.
"""
def __init__(self, value):
self.value = value
[docs] def resolve(self, context):
return self.value
[docs]class SmartIfTests(unittest.TestCase):
[docs] def setUp(self):
self.true = TestVar(True)
self.false = TestVar(False)
self.high = TestVar(9000)
self.low = TestVar(1)
[docs] def assertCalc(self, calc, context=None):
"""
Test a calculation is True, also checking the inverse "negate" case.
"""
context = context or {}
self.assertTrue(calc.resolve(context))
calc.negate = not calc.negate
self.assertFalse(calc.resolve(context))
[docs] def assertCalcFalse(self, calc, context=None):
"""
Test a calculation is False, also checking the inverse "negate" case.
"""
context = context or {}
self.assertFalse(calc.resolve(context))
calc.negate = not calc.negate
self.assertTrue(calc.resolve(context))
[docs] def test_or(self):
self.assertCalc(Or(self.true))
self.assertCalcFalse(Or(self.false))
self.assertCalc(Or(self.true, self.true))
self.assertCalc(Or(self.true, self.false))
self.assertCalc(Or(self.false, self.true))
self.assertCalcFalse(Or(self.false, self.false))
[docs] def test_and(self):
self.assertCalc(And(self.true, self.true))
self.assertCalcFalse(And(self.true, self.false))
self.assertCalcFalse(And(self.false, self.true))
self.assertCalcFalse(And(self.false, self.false))
[docs] def test_equals(self):
self.assertCalc(Equals(self.low, self.low))
self.assertCalcFalse(Equals(self.low, self.high))
[docs] def test_greater(self):
self.assertCalc(Greater(self.high, self.low))
self.assertCalcFalse(Greater(self.low, self.low))
self.assertCalcFalse(Greater(self.low, self.high))
[docs] def test_greater_or_equal(self):
self.assertCalc(GreaterOrEqual(self.high, self.low))
self.assertCalc(GreaterOrEqual(self.low, self.low))
self.assertCalcFalse(GreaterOrEqual(self.low, self.high))
[docs] def test_in(self):
list_ = TestVar([1, 2, 3])
invalid_list = TestVar(None)
self.assertCalc(In(self.low, list_))
self.assertCalcFalse(In(self.low, invalid_list))
[docs] def test_parse_bits(self):
var = IfParser([True]).parse()
self.assertTrue(var.resolve({}))
var = IfParser([False]).parse()
self.assertFalse(var.resolve({}))
var = IfParser([False, 'or', True]).parse()
self.assertTrue(var.resolve({}))
var = IfParser([False, 'and', True]).parse()
self.assertFalse(var.resolve({}))
var = IfParser(['not', False, 'and', 'not', False]).parse()
self.assertTrue(var.resolve({}))
var = IfParser([1, '=', 1]).parse()
self.assertTrue(var.resolve({}))
var = IfParser([1, '!=', 1]).parse()
self.assertFalse(var.resolve({}))
var = IfParser([3, '>', 2]).parse()
self.assertTrue(var.resolve({}))
var = IfParser([1, '<', 2]).parse()
self.assertTrue(var.resolve({}))
var = IfParser([2, 'not', 'in', [2, 3]]).parse()
self.assertFalse(var.resolve({}))
[docs] def test_boolean(self):
var = IfParser([True, 'and', True, 'and', True]).parse()
self.assertTrue(var.resolve({}))
var = IfParser([False, 'or', False, 'or', True]).parse()
self.assertTrue(var.resolve({}))
var = IfParser([True, 'and', False, 'or', True]).parse()
self.assertTrue(var.resolve({}))
var = IfParser([False, 'or', True, 'and', True]).parse()
self.assertTrue(var.resolve({}))
var = IfParser([True, 'and', True, 'and', False]).parse()
self.assertFalse(var.resolve({}))
var = IfParser([False, 'or', False, 'or', False]).parse()
self.assertFalse(var.resolve({}))
var = IfParser([False, 'or', True, 'and', False]).parse()
self.assertFalse(var.resolve({}))
var = IfParser([False, 'and', True, 'or', False]).parse()
self.assertFalse(var.resolve({}))
OPERATORS = {
'=': (Equals, True),
'==': (Equals, True),
'!=': (Equals, False),
'>': (Greater, True),
'>=': (GreaterOrEqual, True),
'<=': (Greater, False),
'<': (GreaterOrEqual, False),
'or': (Or, True),
'and': (And, True),
'in': (In, True),
}
[docs]class IfParser(object):
error_class = ValueError
def __init__(self, tokens):
self.tokens = tokens
def _get_tokens(self):
return self._tokens
def _set_tokens(self, tokens):
self._tokens = tokens
self.len = len(tokens)
self.pos = 0
tokens = property(_get_tokens, _set_tokens)
[docs] def parse(self):
if self.at_end():
raise self.error_class('No variables provided.')
var1 = self.get_var()
while not self.at_end():
token = self.get_token()
if token == 'not':
if self.at_end():
raise self.error_class('No variable provided after "not".')
token = self.get_token()
negate = True
else:
negate = False
if token not in OPERATORS:
raise self.error_class('%s is not a valid operator.' % token)
if self.at_end():
raise self.error_class('No variable provided after "%s"' % token)
op, true = OPERATORS[token]
if not true:
negate = not negate
var2 = self.get_var()
var1 = op(var1, var2, negate=negate)
return var1
[docs] def get_token(self):
token = self.tokens[self.pos]
self.pos += 1
return token
[docs] def at_end(self):
return self.pos >= self.len
[docs] def create_var(self, value):
return TestVar(value)
[docs] def get_var(self):
token = self.get_token()
if token == 'not':
if self.at_end():
raise self.error_class('No variable provided after "not".')
token = self.get_token()
return Or(self.create_var(token), negate=True)
return self.create_var(token)
# ===============================================================================
# Actual templatetag code.
# ===============================================================================
[docs]class TemplateIfParser(IfParser):
error_class = template.TemplateSyntaxError
def __init__(self, parser, *args, **kwargs):
self.template_parser = parser
super(TemplateIfParser, self).__init__(*args, **kwargs)
[docs] def create_var(self, value):
return self.template_parser.compile_filter(value)
[docs]class SmartIfNode(template.Node):
def __init__(self, var, nodelist_true, nodelist_false=None):
self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false
self.var = var
[docs] def render(self, context):
if self.var.resolve(context):
return self.nodelist_true.render(context)
if self.nodelist_false:
return self.nodelist_false.render(context)
return ''
def __repr__(self):
return "<Smart If node>"
def __iter__(self):
for node in self.nodelist_true:
yield node
if self.nodelist_false:
for node in self.nodelist_false:
yield node
[docs] def get_nodes_by_type(self, nodetype):
nodes = []
if isinstance(self, nodetype):
nodes.append(self)
nodes.extend(self.nodelist_true.get_nodes_by_type(nodetype))
if self.nodelist_false:
nodes.extend(self.nodelist_false.get_nodes_by_type(nodetype))
return nodes
# @register.tag('if')
[docs]def smart_if(parser, token):
'''
A smarter {% if %} tag for django templates.
While retaining current Django functionality, it also handles equality,
greater than and less than operators. Some common case examples::
{% if articles|length >= 5 %}...{% endif %}
{% if "ifnotequal tag" != "beautiful" %}...{% endif %}
Arguments and operators _must_ have a space between them, so
``{% if 1>2 %}`` is not a valid smart if tag.
All supported operators are: ``or``, ``and``, ``in``, ``=`` (or ``==``),
``!=``, ``>``, ``>=``, ``<`` and ``<=``.
'''
bits = token.split_contents()[1:]
var = TemplateIfParser(parser, bits).parse()
nodelist_true = parser.parse(('else', 'endsmart_if'))
token = parser.next_token()
if token.contents == 'else':
nodelist_false = parser.parse(('endsmart_if',))
parser.delete_first_token()
else:
nodelist_false = None
return SmartIfNode(var, nodelist_true, nodelist_false)
# ==========================
ifinlist = register.tag(smart_if)
# ==========================
# Based on code found here:
# http://stackoverflow.com/questions/2024660/django-sort-dict-in-template
#
# Required since dict.items|dictsort doesn't seem to work
# when iterating over the keys with a for loop
[docs]@register.filter(name='sort')
def listsort(value):
if isinstance(value, dict):
new_dict = OrderedDict()
key_list = list(value.keys())
key_list.sort()
for key in key_list:
new_dict[key] = value[key]
return new_dict
elif isinstance(value, list):
new_list = list(value)
new_list.sort()
return new_list
else:
return value
listsort.is_safe = True