diff --git a/publik_django_templatetags/publik/templatetags/publik.py b/publik_django_templatetags/publik/templatetags/publik.py
index d56990f..26932bc 100644
--- a/publik_django_templatetags/publik/templatetags/publik.py
+++ b/publik_django_templatetags/publik/templatetags/publik.py
@@ -14,8 +14,14 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+import math
+from decimal import Decimal
+from decimal import DivisionByZero as DecimalDivisionByZero
+from decimal import InvalidOperation as DecimalInvalidOperation
+
from django import template
from django.template import defaultfilters
+from django.utils.encoding import force_text
register = template.Library()
@@ -48,6 +54,11 @@ def as_list(obj):
return list(obj)
+@register.filter
+def split(string, separator=' '):
+ return (force_text(string) or '').split(separator)
+
+
@register.filter
def first(value):
try:
@@ -62,3 +73,89 @@ def last(value):
return defaultfilters.last(value)
except TypeError:
return ''
+
+
+def parse_decimal(value, default=Decimal(0)):
+ if isinstance(value, str):
+ # replace , by . for French users comfort
+ value = value.replace(',', '.')
+ try:
+ return Decimal(value).quantize(Decimal('1.0000')).normalize()
+ except (ArithmeticError, TypeError):
+ return default
+
+
+@register.filter(is_safe=False)
+def decimal(value, arg=None):
+ if not isinstance(value, Decimal):
+ value = parse_decimal(value)
+ if arg is None:
+ return value
+ return defaultfilters.floatformat(value, arg=arg)
+
+
+@register.filter
+def add(term1, term2):
+ '''replace the "add" native django filter'''
+
+ if term1 is None:
+ term1 = ''
+ if term2 is None:
+ term2 = ''
+ term1_decimal = parse_decimal(term1, default=None)
+ term2_decimal = parse_decimal(term2, default=None)
+
+ if term1_decimal is not None and term2_decimal is not None:
+ return term1_decimal + term2_decimal
+ if term1 == '' and term2_decimal is not None:
+ return term2_decimal
+ if term2 == '' and term1_decimal is not None:
+ return term1_decimal
+ return defaultfilters.add(term1, term2)
+
+
+@register.filter
+def subtract(term1, term2):
+ return parse_decimal(term1) - parse_decimal(term2)
+
+
+@register.filter
+def multiply(term1, term2):
+ return parse_decimal(term1) * parse_decimal(term2)
+
+
+@register.filter
+def divide(term1, term2):
+ try:
+ return parse_decimal(term1) / parse_decimal(term2)
+ except DecimalInvalidOperation:
+ return ''
+ except DecimalDivisionByZero:
+ return ''
+
+
+@register.filter
+def ceil(value):
+ '''the smallest integer value greater than or equal to value'''
+ return decimal(math.ceil(parse_decimal(value)))
+
+
+@register.filter
+def floor(value):
+ return decimal(math.floor(parse_decimal(value)))
+
+
+@register.filter(name='abs')
+def abs_(value):
+ return decimal(abs(parse_decimal(value)))
+
+
+@register.filter(name='sum')
+def sum_(list_):
+ if isinstance(list_, str):
+ # do not consider string as iterable, to avoid misusage
+ return ''
+ try:
+ return sum(parse_decimal(term) for term in list_)
+ except TypeError: # list_ is not iterable
+ return ''
diff --git a/tests/test_publik.py b/tests/test_publik.py
index c3dd2e7..8904b16 100644
--- a/tests/test_publik.py
+++ b/tests/test_publik.py
@@ -54,6 +54,15 @@ def test_getlist():
assert t.render(context) == 'None,baz,'
+def test_split():
+ t = Template('{% for x in plop|split %}{{x}}
{% endfor %}')
+ assert t.render(Context({'plop': 'ab cd ef'})) == 'ab
cd
ef
'
+ t = Template('{% for x in plop|split:"|" %}{{x}} {% endfor %}')
+ assert t.render(Context({'plop': 'ab|cd|ef'})) == 'ab cd ef '
+ t = Template('{% for x in plop|split:"|" %}{{x}} {% endfor %}')
+ assert t.render(Context({'plop': 42})) == '42 '
+
+
def test_first():
t = Template('{{ foo|first }}')
@@ -84,3 +93,208 @@ def test_last():
context = Context({'foo': None})
assert t.render(context) == ''
+
+
+def test_decimal():
+ tmpl = Template('{{ plop|decimal }}')
+ assert tmpl.render(Context({'plop': 'toto'})) == '0'
+ assert tmpl.render(Context({'plop': '3.14'})) == '3.14'
+ assert tmpl.render(Context({'plop': '3,14'})) == '3.14'
+ assert tmpl.render(Context({'plop': 3.14})) == '3.14'
+ assert tmpl.render(Context({'plop': 12345.678})) == '12345.678'
+ assert tmpl.render(Context({'plop': None})) == '0'
+ assert tmpl.render(Context({'plop': 0})) == '0'
+
+ tmpl = Template('{{ plop|decimal:3 }}')
+ assert tmpl.render(Context({'plop': '3.14'})) == '3.140'
+ assert tmpl.render(Context({'plop': None})) == '0.000'
+ tmpl = Template('{{ plop|decimal:"3" }}')
+ assert tmpl.render(Context({'plop': '3.14'})) == '3.140'
+ assert tmpl.render(Context({'plop': None})) == '0.000'
+
+ tmpl = Template('{% if plop|decimal > 2 %}hello{% endif %}')
+ assert tmpl.render(Context({'plop': 3})) == 'hello'
+ assert tmpl.render(Context({'plop': '3'})) == 'hello'
+ assert tmpl.render(Context({'plop': 2.001})) == 'hello'
+ assert tmpl.render(Context({'plop': '2.001'})) == 'hello'
+ assert tmpl.render(Context({'plop': 1})) == ''
+ assert tmpl.render(Context({'plop': 1.99})) == ''
+ assert tmpl.render(Context({'plop': '1.99'})) == ''
+ assert tmpl.render(Context({'plop': 'x'})) == ''
+ assert tmpl.render(Context({'plop': None})) == ''
+ assert tmpl.render(Context({'plop': 0})) == ''
+
+ tmpl = Template('{% if "3"|decimal == 3 %}hello{% endif %}')
+ assert tmpl.render(Context()) == 'hello'
+ tmpl = Template('{% if "3"|decimal == 3.0 %}hello{% endif %}')
+ assert tmpl.render(Context()) == 'hello'
+ tmpl = Template('{% if 3|decimal == 3 %}hello{% endif %}')
+ assert tmpl.render(Context()) == 'hello'
+ tmpl = Template('{% if 3.0|decimal == 3 %}hello{% endif %}')
+ assert tmpl.render(Context()) == 'hello'
+ tmpl = Template('{% if 3|decimal|decimal == 3 %}hello{% endif %}')
+ assert tmpl.render(Context()) == 'hello'
+
+
+def test_add():
+ tmpl = Template('{{ term1|add:term2 }}')
+
+ # using strings
+ assert tmpl.render(Context({'term1': '1.1', 'term2': 0})) == '1.1'
+ assert tmpl.render(Context({'term1': 'not a number', 'term2': 1.2})) == ''
+ assert tmpl.render(Context({'term1': 0.3, 'term2': "1"})) == '1.3'
+ assert tmpl.render(Context({'term1': 1.4, 'term2': "not a number"})) == ''
+
+ # add
+ assert tmpl.render(Context({'term1': 4, 'term2': -0.9})) == '3.1'
+ assert tmpl.render(Context({'term1': '4', 'term2': -0.8})) == '3.2'
+ assert tmpl.render(Context({'term1': 4, 'term2': '-0.7'})) == '3.3'
+ assert tmpl.render(Context({'term1': '4', 'term2': '-0.6'})) == '3.4'
+ assert tmpl.render(Context({'term1': '', 'term2': 3.5})) == '3.5'
+ assert tmpl.render(Context({'term1': None, 'term2': 3.5})) == '3.5'
+ assert tmpl.render(Context({'term1': 3.6, 'term2': ''})) == '3.6'
+ assert tmpl.render(Context({'term1': '', 'term2': ''})) == ''
+ assert tmpl.render(Context({'term1': 3.6, 'term2': None})) == '3.6'
+ assert tmpl.render(Context({'term1': 0, 'term2': ''})) == '0'
+ assert tmpl.render(Context({'term1': '', 'term2': 0})) == '0'
+ assert tmpl.render(Context({'term1': 0, 'term2': 0})) == '0'
+
+ # if term is '' or None and other term is decimal
+ assert tmpl.render(Context({'term1': '', 'term2': 2.2})) == '2.2'
+ assert tmpl.render(Context({'term1': None, 'term2': 2.2})) == '2.2'
+ assert tmpl.render(Context({'term1': 2.2, 'term2': ''})) == '2.2'
+ assert tmpl.render(Context({'term1': 2.2, 'term2': None})) == '2.2'
+
+ # add using ',' instead of '.' decimal separator
+ assert tmpl.render(Context({'term1': '1,1', 'term2': '2,2'})) == '3.3'
+ assert tmpl.render(Context({'term1': '1,1', 'term2': '2.2'})) == '3.3'
+ assert tmpl.render(Context({'term1': '1,1', 'term2': 2.2})) == '3.3'
+ assert tmpl.render(Context({'term1': '1,1', 'term2': 0})) == '1.1'
+ assert tmpl.render(Context({'term1': '1,1', 'term2': ''})) == '1.1'
+ assert tmpl.render(Context({'term1': '1,1', 'term2': None})) == '1.1'
+ assert tmpl.render(Context({'term1': '1.1', 'term2': '2,2'})) == '3.3'
+ assert tmpl.render(Context({'term1': 1.1, 'term2': '2,2'})) == '3.3'
+ assert tmpl.render(Context({'term1': 0, 'term2': '2,2'})) == '2.2'
+ assert tmpl.render(Context({'term1': '', 'term2': '2,2'})) == '2.2'
+ assert tmpl.render(Context({'term1': None, 'term2': '2,2'})) == '2.2'
+
+ # fallback to Django native add filter
+ assert tmpl.render(Context({'term1': 'foo', 'term2': 'bar'})) == 'foobar'
+ assert tmpl.render(Context({'term1': 'foo', 'term2': ''})) == 'foo'
+ assert tmpl.render(Context({'term1': 'foo', 'term2': None})) == 'foo'
+ assert tmpl.render(Context({'term1': 'foo', 'term2': 0})) == ''
+ assert tmpl.render(Context({'term1': '', 'term2': 'bar'})) == 'bar'
+ assert tmpl.render(Context({'term1': '', 'term2': ''})) == ''
+ assert tmpl.render(Context({'term1': '', 'term2': None})) == ''
+ assert tmpl.render(Context({'term1': None, 'term2': 'bar'})) == 'bar'
+ assert tmpl.render(Context({'term1': None, 'term2': ''})) == ''
+ assert tmpl.render(Context({'term1': None, 'term2': None})) == ''
+ assert tmpl.render(Context({'term1': 0, 'term2': 'bar'})) == ''
+
+
+def test_substract():
+ tmpl = Template('{{ term1|subtract:term2 }}')
+ assert tmpl.render(Context({'term1': 5.1, 'term2': 1})) == '4.1'
+ assert tmpl.render(Context({'term1': '5.2', 'term2': 1})) == '4.2'
+ assert tmpl.render(Context({'term1': 5.3, 'term2': '1'})) == '4.3'
+ assert tmpl.render(Context({'term1': '5.4', 'term2': '1'})) == '4.4'
+ assert tmpl.render(Context({'term1': '', 'term2': -4.5})) == '4.5'
+ assert tmpl.render(Context({'term1': 4.6, 'term2': ''})) == '4.6'
+ assert tmpl.render(Context({'term1': '', 'term2': ''})) == '0'
+ assert tmpl.render(Context({'term1': 0, 'term2': ''})) == '0'
+ assert tmpl.render(Context({'term1': '', 'term2': 0})) == '0'
+ assert tmpl.render(Context({'term1': 0, 'term2': 0})) == '0'
+
+
+def test_multiply():
+ tmpl = Template('{{ term1|multiply:term2 }}')
+ assert tmpl.render(Context({'term1': '3', 'term2': '2'})) == '6'
+ assert tmpl.render(Context({'term1': 2.5, 'term2': 2})) == '5.0'
+ assert tmpl.render(Context({'term1': '2.5', 'term2': 2})) == '5.0'
+ assert tmpl.render(Context({'term1': 2.5, 'term2': '2'})) == '5.0'
+ assert tmpl.render(Context({'term1': '2.5', 'term2': '2'})) == '5.0'
+ assert tmpl.render(Context({'term1': '', 'term2': '2'})) == '0'
+ assert tmpl.render(Context({'term1': 2.5, 'term2': ''})) == '0.0'
+ assert tmpl.render(Context({'term1': '', 'term2': ''})) == '0'
+ assert tmpl.render(Context({'term1': 0, 'term2': ''})) == '0'
+ assert tmpl.render(Context({'term1': '', 'term2': 0})) == '0'
+ assert tmpl.render(Context({'term1': 0, 'term2': 0})) == '0'
+
+
+def test_divide():
+ tmpl = Template('{{ term1|divide:term2 }}')
+ assert tmpl.render(Context({'term1': 16, 'term2': 2})) == '8'
+ assert tmpl.render(Context({'term1': 6, 'term2': 0.75})) == '8'
+ assert tmpl.render(Context({'term1': '6', 'term2': 0.75})) == '8'
+ assert tmpl.render(Context({'term1': 6, 'term2': '0.75'})) == '8'
+ assert tmpl.render(Context({'term1': '6', 'term2': '0.75'})) == '8'
+ assert tmpl.render(Context({'term1': '', 'term2': '2'})) == '0'
+ assert tmpl.render(Context({'term1': 6, 'term2': ''})) == ''
+ assert tmpl.render(Context({'term1': '', 'term2': ''})) == ''
+ assert tmpl.render(Context({'term1': 0, 'term2': ''})) == ''
+ assert tmpl.render(Context({'term1': '', 'term2': 0})) == ''
+ assert tmpl.render(Context({'term1': 0, 'term2': 0})) == ''
+ tmpl = Template('{{ term1|divide:term2|decimal:2 }}')
+ assert tmpl.render(Context({'term1': 2, 'term2': 3})) == '0.67'
+
+
+def test_ceil():
+ # ceil
+ tmpl = Template('{{ value|ceil }}')
+ assert tmpl.render(Context({'value': 3.14})) == '4'
+ assert tmpl.render(Context({'value': 3.99})) == '4'
+ assert tmpl.render(Context({'value': -3.14})) == '-3'
+ assert tmpl.render(Context({'value': -3.99})) == '-3'
+ assert tmpl.render(Context({'value': 0})) == '0'
+ assert tmpl.render(Context({'value': '3.14'})) == '4'
+ assert tmpl.render(Context({'value': '3.99'})) == '4'
+ assert tmpl.render(Context({'value': '-3.14'})) == '-3'
+ assert tmpl.render(Context({'value': '-3.99'})) == '-3'
+ assert tmpl.render(Context({'value': '0'})) == '0'
+ assert tmpl.render(Context({'value': 'not a number'})) == '0'
+ assert tmpl.render(Context({'value': ''})) == '0'
+ assert tmpl.render(Context({'value': None})) == '0'
+
+
+def test_floor():
+ # floor
+ tmpl = Template('{{ value|floor }}')
+ assert tmpl.render(Context({'value': 3.14})) == '3'
+ assert tmpl.render(Context({'value': 3.99})) == '3'
+ assert tmpl.render(Context({'value': -3.14})) == '-4'
+ assert tmpl.render(Context({'value': -3.99})) == '-4'
+ assert tmpl.render(Context({'value': 0})) == '0'
+ assert tmpl.render(Context({'value': '3.14'})) == '3'
+ assert tmpl.render(Context({'value': '3.99'})) == '3'
+ assert tmpl.render(Context({'value': '-3.14'})) == '-4'
+ assert tmpl.render(Context({'value': '-3.99'})) == '-4'
+ assert tmpl.render(Context({'value': '0'})) == '0'
+ assert tmpl.render(Context({'value': 'not a number'})) == '0'
+ assert tmpl.render(Context({'value': ''})) == '0'
+ assert tmpl.render(Context({'value': None})) == '0'
+
+
+def test_abs():
+ tmpl = Template('{{ value|abs }}')
+ assert tmpl.render(Context({'value': 3.14})) == '3.14'
+ assert tmpl.render(Context({'value': -3.14})) == '3.14'
+ assert tmpl.render(Context({'value': 0})) == '0'
+ assert tmpl.render(Context({'value': '3.14'})) == '3.14'
+ assert tmpl.render(Context({'value': '-3.14'})) == '3.14'
+ assert tmpl.render(Context({'value': '0'})) == '0'
+ assert tmpl.render(Context({'value': 'not a number'})) == '0'
+ assert tmpl.render(Context({'value': ''})) == '0'
+ assert tmpl.render(Context({'value': None})) == '0'
+
+
+def test_sum():
+ t = Template('{{ "2 29.5 9,5 .5"|split|sum }}')
+ assert t.render(Context()) == '41.5'
+ t = Template('{{ list|sum }}')
+ assert t.render(Context({'list': [1, 2, '3']})) == '6'
+ assert t.render(Context({'list': [1, 2, 'x']})) == '3'
+ assert t.render(Context({'list': [None, 2.0, 'x']})) == '2'
+ assert t.render(Context({'list': []})) == '0'
+ assert t.render(Context({'list': None})) == '' # list is not iterable
+ assert t.render(Context({'list': '123'})) == '' # consider string as not iterable
+ assert t.render(Context()) == ''