From b3cdb133fe55cd397143f3021bc0d720a4e62690 Mon Sep 17 00:00:00 2001
From: Thomas NOEL <tnoel@entrouvert.com>
Date: Mon, 24 Dec 2018 16:15:10 +0100
Subject: [PATCH] templatetags: add maths filters (#29337)

---
 tests/test_templates.py           | 127 ++++++++++++++++++++++++++++++
 wcs/qommon/templatetags/qommon.py |  41 ++++++++++
 2 files changed, 168 insertions(+)

diff --git a/tests/test_templates.py b/tests/test_templates.py
index 864b4e4b..cb4f4c59 100644
--- a/tests/test_templates.py
+++ b/tests/test_templates.py
@@ -2,6 +2,9 @@
 
 import pytest
 
+import datetime
+from decimal import Decimal
+
 from quixote import cleanup
 from qommon.template import Template, TemplateError
 from wcs.conditions import Condition
@@ -257,3 +260,127 @@ def test_decimal_templatetag():
     assert tmpl.render({'plop': '1.99'}) == ''
     assert tmpl.render({'plop': 'x'}) == 'hello'
     assert tmpl.render({'plop': None}) == 'hello'  # will be str > int, always True.
+
+def test_add_templatetag():
+    for a in ('2', '"2"', '2.0', '"2.0"'):
+        for b in (3, '"3"', '3.0', '"3.0"'):
+            assert Template('{{ %s|add:%s }}' % (a, b)).render() == '5'
+        for b in (3.01, '"3.01"'):
+            assert Template('{{ %s|add:%s }}' % (a, b)).render() == '5.01'
+        for b in ('""', '"x"'):
+            assert Template('{{ %s|add:%s }}' % (a, b)).render() == ''
+            assert Template('{{ %s|add:%s }}' % (b, a)).render() == ''
+        # no 'foo' variable: template error
+        assert Template('{{ %s|add:foo }}' % a).render() == '{{ %s|add:foo }}' % a
+    for a in ('2.01', '"2.01"'):
+        for b in (3, '"3"', '3.0', '"3.0"'):
+            assert Template('{{ %s|add:%s }}' % (a, b)).render() == '5.01'
+        for b in (3.01, '"3.01"'):
+            assert Template('{{ %s|add:%s }}' % (a, b)).render() == '5.02'
+        for b in ('""', '"x"'):
+            assert Template('{{ %s|add:%s }}' % (a, b)).render() == ''
+            assert Template('{{ %s|add:%s }}' % (b, a)).render() == ''
+        assert Template('{{ %s|add:foo }}' % a).render() == '{{ %s|add:foo }}' % a
+
+    for a in (2, '2', '2.0', 2.0, Decimal(2)):
+        for b in (3, '3', '3.0', 3.0, Decimal(3)):
+            assert Template('{{ a|add:b }}').render({'a': a, 'b': b}) == '5'
+        for b in (3.01, '3.01', Decimal(3.01)):
+            assert Template('{{ a|add:b }}').render({'a': a, 'b': b}) == '5.01'
+        for b in ('', 'x', None):
+            assert Template('{{ a|add:b }}').render({'a': a, 'b': b}) == ''
+            assert Template('{{ b|add:a }}').render({'a': a, 'b': b}) == ''
+        assert Template('{{ a|add:foo }}').render({'a': a}) == '{{ a|add:foo }}'
+    for a in (2.01, '2.01', Decimal(2.01)):
+        for b in (3, '3', '3.0', 3.0, Decimal(3)):
+            assert Template('{{ a|add:b }}').render({'a': a, 'b': b}) == '5.01'
+        for b in (3.01, '3.01', Decimal(3.01)):
+            assert Template('{{ a|add:b }}').render({'a': a, 'b': b}) == '5.02'
+        for b in ('', 'x', None):
+            assert Template('{{ a|add:b }}').render({'a': a, 'b': b}) == ''
+            assert Template('{{ b|add:a }}').render({'a': a, 'b': b}) == ''
+        assert Template('{{ a|add:foo }}').render({'a': a}) == '{{ a|add:foo }}'
+
+    assert Template('{{ ""|add:"" }}').render() == ''
+    assert Template('{{ ""|add:"x" }}').render() == ''
+    assert Template('{{ "x"|add:"x" }}').render() == ''
+    assert Template('{{ "x"|add:"" }}').render() == ''
+    assert Template('{{ foo|add:2 }}').render() == ''
+    assert Template('{{ foo|add:bar }}').render() == '{{ foo|add:bar }}'
+
+def test_add_templatetag_dates():
+    assert Template('{{ "2017-12-21"|date|add:"2" }}').render() == 'Dec. 23, 2017'
+    assert Template('{{ "2017-12-21"|date|add:2 }}').render() == 'Dec. 23, 2017'
+    assert Template('{{ "2017-12-21"|date|add:"2d" }}').render() == 'Dec. 23, 2017'
+    assert Template('{{ "2017-12-21"|date|add:"2 days" }}').render() == 'Dec. 23, 2017'
+    assert Template('{{ "2017-12-21"|date|add:"2 DAYS" }}').render() == 'Dec. 23, 2017'
+    assert Template('{{ "2017-12-21"|date|add:"2j" }}').render() == 'Dec. 23, 2017'
+    assert Template('{{ "2017-12-21"|datetime|add:"2" }}').render() == 'Dec. 23, 2017, midnight'
+    # forgot cast -> not a date -> no result
+    assert Template('{{ "2017-12-21"|add:"2" }}').render() == ''
+    assert Template('{{ "2017-12-21 10:32"|add:"2" }}').render() == ''
+    # not a date -> cannot cast -> no result
+    assert Template('{{ "foobar"|date|add:"2" }}').render() == ''
+    assert Template('{{ "foobar"|datetime|add:"2" }}').render() == ''
+
+    assert Template('{{ "2017-12-21 10:32"|datetime|add:2 }}').render() == 'Dec. 23, 2017, 10:32 a.m.'
+    assert Template('{{ "2017-12-21 10:32"|date|add:2 }}').render() == 'Dec. 23, 2017'
+    assert Template('{{ "2017-12-21 10:32"|datetime|add:2.0 }}').render() == 'Dec. 23, 2017, 10:32 a.m.'
+    assert Template('{{ "2017-12-21 10:32"|datetime|add:-2 }}').render() == 'Dec. 19, 2017, 10:32 a.m.'
+    assert Template('{{ "2017-12-21 10:32"|datetime|add:-2.0 }}').render() == 'Dec. 19, 2017, 10:32 a.m.'
+    assert Template('{{ "2017-12-21 10:32"|datetime|add:"2" }}').render() == 'Dec. 23, 2017, 10:32 a.m.'
+    assert Template('{{ "2017-12-21 10:32"|datetime|add:"2.0" }}').render() == 'Dec. 23, 2017, 10:32 a.m.'
+    assert Template('{{ "2017-12-21 10:32"|datetime|add:"-2" }}').render() == 'Dec. 19, 2017, 10:32 a.m.'
+    assert Template('{{ "2017-12-21 10:32"|datetime|add:"-2.0" }}').render() == 'Dec. 19, 2017, 10:32 a.m.'
+    assert Template('{{ "2017-12-21 10:32"|datetime|add:"2.5" }}').render() == 'Dec. 23, 2017, 10:32 p.m.'
+    assert Template('{{ "2017-12-21 10:32"|datetime|add:"-2.5" }}').render() == 'Dec. 18, 2017, 10:32 p.m.'
+
+    # with units
+    assert Template('{{ "2017-12-21 10:32"|datetime|add:"2h" }}').render() == 'Dec. 21, 2017, 12:32 p.m.'
+    assert Template('{{ "2017-12-21 10:32"|datetime|add:"-2h" }}').render() == 'Dec. 21, 2017, 8:32 a.m.'
+    assert Template('{{ "2017-12-21 10:32"|datetime|add:"2.5h" }}').render() == 'Dec. 21, 2017, 1:02 p.m.'
+    assert Template('{{ "2017-12-21 10:32"|datetime|add:"-2.5h" }}').render() == 'Dec. 21, 2017, 8:02 a.m.'
+    assert Template('{{ "2017-12-21 10:32"|datetime|add:"20m" }}').render() == 'Dec. 21, 2017, 10:52 a.m.'
+    assert Template('{{ "2017-12-21 10:32"|datetime|add:"20min" }}').render() == 'Dec. 21, 2017, 10:52 a.m.'
+    assert Template('{{ "2017-12-21 10:32"|datetime|add:"-20m" }}').render() == 'Dec. 21, 2017, 10:12 a.m.'
+    assert Template('{{ "2017-12-21 10:32"|datetime|add:"120s" }}').render() == 'Dec. 21, 2017, 10:34 a.m.'
+    assert Template('{{ "2017-12-21 10:32"|datetime|add:"120seconds" }}').render() == 'Dec. 21, 2017, 10:34 a.m.'
+    assert Template('{{ "2017-12-21 10:32"|datetime|add:"-120sec" }}').render() == 'Dec. 21, 2017, 10:30 a.m.'
+
+    # compare after math
+    assert Template('{% if "2017-12-21"|date|add:"2" > "2017-12-21"|date %}OK{% endif %}').render() == 'OK'
+    assert Template('{% if "2017-12-21"|datetime|add:"2" > "2017-12-21"|datetime %}OK{% endif %}').render() == 'OK'
+    assert Template('{% if "2017-12-21"|date|add:"1" == "2017-12-22"|date %}OK{% endif %}').render() == 'OK'
+    assert Template('{% if "2017-12-21"|datetime|add:"1" == "2017-12-22"|datetime %}OK{% endif %}').render() == 'OK'
+    assert Template('{% if "2017-12-21 12:00"|datetime|add:"12h" == "2017-12-22"|datetime %}OK{% endif %}').render() == 'OK'
+
+    foo = "2017-12-21"
+    bar = "2017-12-23"
+    assert Template('{% if foo|date|add:"2" > foo|date %}OK{% endif %}').render({'foo': foo}) == 'OK'
+    assert Template('{% if foo|date|add:"1" < bar|date %}OK{% endif %}').render({'foo': foo, 'bar': bar}) == 'OK'
+    assert Template('{% if foo|date|add:"2" == bar|date %}OK{% endif %}').render({'foo': foo, 'bar': bar}) == 'OK'
+    assert Template('{% if foo|date|add:"3" > bar|date %}OK{% endif %}').render({'foo': foo, 'bar': bar}) == 'OK'
+    assert Template('{% if foo|date|add:"3" == bar|date|add:"1" %}OK{% endif %}').render({'foo': foo, 'bar': bar}) == 'OK'
+    assert Template('{% if foo|date|add:"2" > "2017-12-22"|date %}OK{% endif %}').render({'foo': foo}) == 'OK'
+    # with units
+    assert Template('{% if foo|date|add:"2s" == foo|date %}OK{% endif %}').render({'foo': foo}) == 'OK'
+    assert Template('{% if foo|date|add:"2m" == foo|date %}OK{% endif %}').render({'foo': foo}) == 'OK'
+    assert Template('{% if foo|date|add:"2h" == foo|date %}OK{% endif %}').render({'foo': foo}) == 'OK'
+    assert Template('{% if foo|date|add:"0j" == foo|date %}OK{% endif %}').render({'foo': foo}) == 'OK'
+    assert Template('{% if foo|date|add:"25h" > foo|date %}OK{% endif %}').render({'foo': foo}) == 'OK'
+    assert Template('{% if foo|date|add:"1441min" > foo|date %}OK{% endif %}').render({'foo': foo}) == 'OK'
+    assert Template('{% if foo|date|add:"25h" > foo|date %}OK{% endif %}').render({'foo': foo}) == 'OK'
+    assert Template('{% if foo|date|add:"80 hours" > bar|date %}OK{% endif %}').render({'foo': foo, 'bar': bar}) == 'OK'
+
+    # incompatible types: always KO
+    assert Template('{% if "2017-12-21"|date|add:"2" == "2017-12-21" %}OK{% else %}KO{% endif %}').render() == 'KO'
+    assert Template('{% if "2017-12-21"|date|add:"2" > "2017-12-21" %}OK{% else %}KO{% endif %}').render() == 'KO'
+    assert Template('{% if "2017-12-21"|date|add:"2" < "2017-12-21" %}OK{% else %}KO{% endif %}').render() == 'KO'
+    # Python cannot compare datetime.date and datetime.datetime !
+    assert Template('{% if "2017-12-21"|date|add:"2" > "2017-12-21"|datetime %}OK{% else %}KO{% endif %}').render() == 'KO'
+
+    # forgot cast -> strange results ! All of these tests are just
+    # side effects of comparing "" and "2017-12-21"
+    assert Template('{% if "2017-12-21"|add:"2" == "2017-12-21" %}OK{% else %}KO{% endif %}').render() == 'KO'
+    assert Template('{% if "2017-12-21"|add:"2" > "2017-12-21" %}OK{% else %}KO{% endif %}').render() == 'KO'
+    assert Template('{% if "2017-12-21"|add:"2" < "2017-12-21" %}OK{% else %}KO{% endif %}').render() == 'OK'
diff --git a/wcs/qommon/templatetags/qommon.py b/wcs/qommon/templatetags/qommon.py
index fc016e64..65e7080f 100644
--- a/wcs/qommon/templatetags/qommon.py
+++ b/wcs/qommon/templatetags/qommon.py
@@ -16,6 +16,7 @@
 
 import datetime
 from decimal import Decimal
+import re
 
 from django import template
 from django.template import defaultfilters
@@ -122,6 +123,46 @@ def decimal(value, arg=None):
         value = parse_decimal(value)
     return defaultfilters.floatformat(value, arg=arg)
 
+def add_date(value, delta):
+    time_unit = 'd'
+    if isinstance(delta, basestring):
+        delta = delta.strip().lower()
+        # replace , by . for French users comfort
+        delta = delta.replace(',', '.')
+        re_delta = re.match('^([+-]?[\d\.]+)\s*([smhd]?).*', delta)
+        if not re_delta:
+            return value
+        delta, time_unit = re_delta.group(1), re_delta.group(2)
+    try:
+        delta = int(delta)
+    except ValueError:
+        try:
+            delta = float(delta)
+        except ValueError:
+            return ''
+    if time_unit == 'h':
+        delta = datetime.timedelta(hours=delta)
+    elif time_unit == 'm':
+        delta = datetime.timedelta(minutes=delta)
+    elif time_unit == 's':
+        delta = datetime.timedelta(seconds=delta)
+    else:
+        delta = datetime.timedelta(days=delta)
+    return value + delta
+
+@register.filter(expects_localtime=True, is_safe=False)
+def add(value, term):
+    if isinstance(value, (datetime.datetime, datetime.date)):
+        return add_date(value, term)
+
+    value = parse_decimal(value)
+    if value is None:
+        return ''
+    term = parse_decimal(term)
+    if term is None:
+        return ''
+    return value + term
+
 @register.simple_tag
 def standard_text(text_id):
     return mark_safe(TextsDirectory.get_html_text(str(text_id)))
-- 
2.19.2

