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()) == ''