From bdfb5af5254404a2dc561c4ab40ec53e08dfe2cd 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           | 117 ++++++++++++++++++++++++++++++
 wcs/qommon/templatetags/qommon.py |  46 ++++++++++++
 2 files changed, 163 insertions(+)

diff --git a/tests/test_templates.py b/tests/test_templates.py
index 864b4e4b..a92b8c94 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,117 @@ 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() == '2'
+            assert Template('{{ %s|add:%s }}' % (b, a)).render() == '2'
+        # 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() == '2.01'
+            assert Template('{{ %s|add:%s }}' % (b, a)).render() == '2.01'
+        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}) == '2'
+            assert Template('{{ b|add:a }}').render({'a': a, 'b': b}) == '2'
+        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}) == '2.01'
+            assert Template('{{ b|add:a }}').render({'a': a, 'b': b}) == '2.01'
+        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() == '2'
+    assert Template('{{ foo|add:"2" }}').render() == '2'
+    assert Template('{{ foo|add:2.01 }}').render() == '2.01'
+    assert Template('{{ foo|add:"2.01" }}').render() == '2.01'
+    assert Template('{{ foo|add:"" }}').render() == ''
+    assert Template('{{ foo|add:"x" }}').render() == ''
+    assert Template('{{ foo|add:bar }}').render() == '{{ foo|add:bar }}'
+
+def test_add_templatetag_dates():
+    assert Template('{{ "2017-12-21"|add:"2" }}').render() == 'Dec. 23, 2017, midnight'
+    assert Template('{{ "2017-12-21"|add:2 }}').render() == 'Dec. 23, 2017, midnight'
+    assert Template('{{ "2017-12-21"|add:"2d" }}').render() == 'Dec. 23, 2017, midnight'
+    assert Template('{{ "2017-12-21"|add:"2 days" }}').render() == 'Dec. 23, 2017, midnight'
+    assert Template('{{ "2017-12-21"|add:"2 DAYS" }}').render() == 'Dec. 23, 2017, midnight'
+    assert Template('{{ "2017-12-21"|add:"2j" }}').render() == 'Dec. 23, 2017, midnight'
+    assert Template('{{ "2017-12-21"|date|add:2 }}').render() == 'Dec. 23, 2017'
+
+    assert Template('{{ "2017-12-21 10:32"|add:2 }}').render() == 'Dec. 23, 2017, 10:32 a.m.'
+    assert Template('{{ "2017-12-21 10:32"|add:2.0 }}').render() == 'Dec. 23, 2017, 10:32 a.m.'
+    assert Template('{{ "2017-12-21 10:32"|add:-2 }}').render() == 'Dec. 19, 2017, 10:32 a.m.'
+    assert Template('{{ "2017-12-21 10:32"|add:-2.0 }}').render() == 'Dec. 19, 2017, 10:32 a.m.'
+    assert Template('{{ "2017-12-21 10:32"|add:"2" }}').render() == 'Dec. 23, 2017, 10:32 a.m.'
+    assert Template('{{ "2017-12-21 10:32"|add:"2.0" }}').render() == 'Dec. 23, 2017, 10:32 a.m.'
+    assert Template('{{ "2017-12-21 10:32"|add:"-2" }}').render() == 'Dec. 19, 2017, 10:32 a.m.'
+    assert Template('{{ "2017-12-21 10:32"|add:"-2.0" }}').render() == 'Dec. 19, 2017, 10:32 a.m.'
+    assert Template('{{ "2017-12-21 10:32"|add:"2.5" }}').render() == 'Dec. 23, 2017, 10:32 p.m.'
+    assert Template('{{ "2017-12-21 10:32"|add:"-2.5" }}').render() == 'Dec. 18, 2017, 10:32 p.m.'
+
+    assert Template('{{ "2017-12-21 10:32"|add:"2h" }}').render() == 'Dec. 21, 2017, 12:32 p.m.'
+    assert Template('{{ "2017-12-21 10:32"|add:"-2h" }}').render() == 'Dec. 21, 2017, 8:32 a.m.'
+    assert Template('{{ "2017-12-21 10:32"|add:"2.5h" }}').render() == 'Dec. 21, 2017, 1:02 p.m.'
+    assert Template('{{ "2017-12-21 10:32"|add:"-2.5h" }}').render() == 'Dec. 21, 2017, 8:02 a.m.'
+
+    assert Template('{{ "2017-12-21 10:32"|add:"20m" }}').render() == 'Dec. 21, 2017, 10:52 a.m.'
+    assert Template('{{ "2017-12-21 10:32"|add:"20min" }}').render() == 'Dec. 21, 2017, 10:52 a.m.'
+    assert Template('{{ "2017-12-21 10:32"|add:"-20m" }}').render() == 'Dec. 21, 2017, 10:12 a.m.'
+
+    assert Template('{{ "2017-12-21 10:32"|add:"120s" }}').render() == 'Dec. 21, 2017, 10:34 a.m.'
+    assert Template('{{ "2017-12-21 10:32"|add:"120seconds" }}').render() == 'Dec. 21, 2017, 10:34 a.m.'
+    assert Template('{{ "2017-12-21 10:32"|add:"-120sec" }}').render() == 'Dec. 21, 2017, 10:30 a.m.'
+
+    # compare dates after add
+    assert Template('{% if "2017-12-21"|date|add:"2" > "2017-12-21"|date %}OK{% endif %}').render() == 'OK'
+    assert Template('{% if "2017-12-21"|add:"2"|date > "2017-12-21"|date %}OK{% endif %}').render() == 'OK'
+    assert Template('{% if "2017-12-21"|add:"2" > "2017-12-21"|datetime %}OK{% endif %}').render() == 'OK'
+    assert Template('{% if "2017-12-21"|add:"1"|date == "2017-12-22"|date %}OK{% endif %}').render() == 'OK'
+    assert Template('{% if "2017-12-21"|add:"1" == "2017-12-22"|datetime %}OK{% endif %}').render() == 'OK'
+    foo = datetime.date(2017, 12, 21)
+    bar = datetime.date(2017, 12, 23)
+    assert Template('{% if foo|add:"2" > foo %}OK{% endif %}').render({'foo': foo}) == 'OK'
+    assert Template('{% if foo|add:"1" < bar %}OK{% endif %}').render({'foo': foo, 'bar': bar}) == 'OK'
+    assert Template('{% if foo|add:"2" == bar %}OK{% endif %}').render({'foo': foo, 'bar': bar}) == 'OK'
+    assert Template('{% if foo|add:"3" > bar %}OK{% endif %}').render({'foo': foo, 'bar': bar}) == 'OK'
+    assert Template('{% if foo|add:"3" == bar|add:"1" %}OK{% endif %}').render({'foo': foo, 'bar': bar}) == 'OK'
+    assert Template('{% if foo|add:"2" > "2017-12-22"|date %}OK{% endif %}').render({'foo': foo}) == 'OK'
+
+    assert Template('{% if foo|add:"2s" == foo|date %}OK{% endif %}').render({'foo': foo}) == 'OK'
+    assert Template('{% if foo|add:"2m" == foo|date %}OK{% endif %}').render({'foo': foo}) == 'OK'
+    assert Template('{% if foo|add:"2h" == foo|date %}OK{% endif %}').render({'foo': foo}) == 'OK'
+    assert Template('{% if foo|add:"0j" == foo|date %}OK{% endif %}').render({'foo': foo}) == 'OK'
+    assert Template('{% if foo|add:"25h" > foo|date %}OK{% endif %}').render({'foo': foo}) == 'OK'
+    assert Template('{% if foo|add:"1441min" > foo|date %}OK{% endif %}').render({'foo': foo}) == 'OK'
+    assert Template('{% if foo|add:"25h" > foo|date %}OK{% endif %}').render({'foo': foo}) == 'OK'
+    assert Template('{% if foo|add:"80 hours" > bar %}OK{% endif %}').render({'foo': foo, 'bar': bar}) == 'OK'
+
+    # incompatible types
+    assert Template('{% if "2017-12-21"|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"|add:"2" > "2017-12-21"|date %}OK{% else %}KO{% endif %}').render() == 'KO'
diff --git a/wcs/qommon/templatetags/qommon.py b/wcs/qommon/templatetags/qommon.py
index fc016e64..4223be06 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,51 @@ 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':
+        return value + datetime.timedelta(hours=delta)
+    if time_unit == 'm':
+        return value + datetime.timedelta(minutes=delta)
+    if time_unit == 's':
+        return value + datetime.timedelta(seconds=delta)
+    return value + datetime.timedelta(days=delta)
+
+@register.filter(expects_localtime=True, is_safe=False)
+def add(value, arg):
+    # guess if value looks like a date
+    if not isinstance(value, (datetime.datetime, datetime.date)):
+        guess_date = parse_datetime(value) or parse_date(value)
+        if guess_date:
+            value = guess_date
+    if isinstance(value, (datetime.datetime, datetime.date)):
+        return add_date(value, arg)
+
+    value = parse_decimal(value)
+    arg = parse_decimal(arg)
+    if value is None and arg is None:
+        return ''
+    if value is None:
+        return arg
+    if arg is None:
+        return value
+    return value + arg
+
 @register.simple_tag
 def standard_text(text_id):
     return mark_safe(TextsDirectory.get_html_text(str(text_id)))
-- 
2.19.2

