From b45f1c495969716fd8cbf7412ff5667376186b41 Mon Sep 17 00:00:00 2001
From: Nicolas ROCHE <nroche@entrouvert.com>
Date: Tue, 19 Feb 2019 14:29:49 +0100
Subject: [PATCH] add mathematics filters (#27709)

- correct "0|decimal" : display '0' instead of '',
- add "|add", "|subtract", "|mutliply" and "|divide" filters,
- current default behaviour is to display empty string on error.
---
 tests/test_formdata.py            | 60 ++++++++++++++++++++++++++++++-
 wcs/qommon/templatetags/qommon.py | 50 +++++++++++++++++++++++---
 2 files changed, 105 insertions(+), 5 deletions(-)

diff --git a/tests/test_formdata.py b/tests/test_formdata.py
index 9f2cc2cf..755f311b 100644
--- a/tests/test_formdata.py
+++ b/tests/test_formdata.py
@@ -7,7 +7,8 @@ import time
 
 from quixote import cleanup
 from quixote.http_request import Upload
-from qommon.template import Template
+import qommon.errors
+from qommon.template import Template, TemplateError
 from qommon.form import PicklableUpload
 from wcs.qommon.http_request import HTTPRequest
 from wcs import fields, formdef
@@ -1290,3 +1291,60 @@ def test_form_digest_date(pub):
     formdata.data = {'0': time.strptime('2015-05-12', '%Y-%m-%d')}
     formdata.store()
     assert formdef.data_class().get(formdata.id).digest == 'plop  plop'
+
+def test_mathematics_operators(pub, variable_test_data):
+    formdef = FormDef.select()[0]
+    formdata = formdef.data_class().select()[0]
+    for mode in (None, 'lazy'):
+        pub.substitutions.reset()
+        pub.substitutions.feed(formdef)
+        with pub.substitutions.temporary_feed(formdata, force_mode=mode):
+            # no arg
+            with pytest.raises(Exception,
+                               match=r'.*Could not find variable at start.*'):
+                WorkflowStatusItem.compute('{{ |decimal}}', raises=True)
+
+            # no param
+            with pytest.raises(Exception,
+                               match=r'.*add requires 2 arguments, 1 provided'):
+                WorkflowStatusItem.compute('{{ 0|add }}', raises=True)
+            with pytest.raises(Exception,
+                               match=r'.*add requires 2 arguments, 1 provided'):
+                WorkflowStatusItem.compute('{{ 0|add: }}', raises=True)
+
+            # string as param
+            assert WorkflowStatusItem.compute('{{ "1,1"|decimal }}', raises=True) == '1.1'
+            assert WorkflowStatusItem.compute('{{ "not a number"|decimal }}', raises=True) == ''
+
+            # strings as arg
+            assert WorkflowStatusItem.compute('{{ 0.3|add:"1" }}', raises=True) == '1.3'
+            assert WorkflowStatusItem.compute('{{ 1.4|add:"not a number" }}', raises=True) == '1.4'
+
+            # decimal
+            assert WorkflowStatusItem.compute('{{ ""|decimal }}', raises=True) == ''
+            assert WorkflowStatusItem.compute('{{ 0|decimal }}', raises=True) == '0'
+            assert WorkflowStatusItem.compute('{{ 2.1|decimal }}', raises=True) == '2.1'
+            assert WorkflowStatusItem.compute('{{ 2.2|decimal:2 }}', raises=True) == '2.20'
+
+            # add
+            assert WorkflowStatusItem.compute('{{ 0|add:"" }}', raises=True) == '0'
+            assert WorkflowStatusItem.compute('{{ ""|add:0 }}', raises=True) == '0'
+            assert WorkflowStatusItem.compute('{{ 3.1|add:"" }}', raises=True) == '3.1'
+            assert WorkflowStatusItem.compute('{{ ""|add:3.2 }}', raises=True) == '3.2'
+            assert WorkflowStatusItem.compute('{{ 4|add:-0.7 }}', raises=True) == '3.3'
+
+            # subtract
+            assert WorkflowStatusItem.compute('{{ 4.1|subtract:"" }}', raises=True) == '4.1'
+            assert WorkflowStatusItem.compute('{{ ""|subtract:-4.2 }}', raises=True) == '4.2'
+            assert WorkflowStatusItem.compute('{{ 5|subtract:0.7 }}', raises=True) == '4.3'
+
+            # multiply
+            assert WorkflowStatusItem.compute('{{ "5,1"|multiply:"" }}', raises=True) == ''
+            assert WorkflowStatusItem.compute('{{ ""|multiply:"5.2"}}', raises=True) == ''
+            assert WorkflowStatusItem.compute('{{ "2.5"|multiply:2 }}', raises=True) == '5.0'
+            assert WorkflowStatusItem.compute('{{ "3"|multiply:2 }}', raises=True) == '6'
+
+            # divide
+            assert WorkflowStatusItem.compute('{{ 6.1|divide:"" }}', raises=True) == ''
+            assert WorkflowStatusItem.compute('{{ ""|divide:6.2}}', raises=True) == ''
+            assert WorkflowStatusItem.compute('{{ 2|divide:3|decimal:2 }}', raises=True) == '0.67'
diff --git a/wcs/qommon/templatetags/qommon.py b/wcs/qommon/templatetags/qommon.py
index 8b507e98..9afaae17 100644
--- a/wcs/qommon/templatetags/qommon.py
+++ b/wcs/qommon/templatetags/qommon.py
@@ -112,7 +112,7 @@ def time(value, arg=None):
     return defaultfilters.date(value, arg=arg)
 
 def parse_decimal(value):
-    if not value:
+    if value is None:
         return None
     if isinstance(value, basestring):
         # replace , by . for French users comfort
@@ -124,11 +124,14 @@ def parse_decimal(value):
 
 @register.filter(is_safe=False)
 def decimal(value, arg=None):
-    if arg is None:
-        return parse_decimal(value) or ''
     if not isinstance(value, Decimal):
         value = parse_decimal(value)
-    return defaultfilters.floatformat(value, arg=arg)
+    if value is None:
+        return ''
+    if arg is None:
+        return value
+    else:
+        return defaultfilters.floatformat(value, arg=arg)
 
 @register.filter(expects_localtime=True, is_safe=False)
 def add_days(value, arg):
@@ -225,3 +228,42 @@ def action_button(context, action_id, label, delay=3):
     }
     token.store()
     return '---===BUTTON:%s:%s===---' % (token.id, label)
+
+@register.filter
+def add(value, arg):
+    v1 = parse_decimal(value)
+    v2 = parse_decimal(arg)
+    if v2 is None:
+        return v1 if v1 is not None else ''
+    if v1 is None:
+        return v2 if v2 is not None else ''
+    return v1 + v2
+
+@register.filter
+def subtract(value, arg):
+    v1 = parse_decimal(value)
+    v2 = parse_decimal(arg)
+    if v2 is None:
+        return v1 if v1 is not None else ''
+    if v1 is None:
+        return -v2 if v2 is not None else ''
+    return v1 - v2
+
+@register.filter
+def multiply(value, arg):
+    v1 = parse_decimal(value)
+    v2 = parse_decimal(arg)
+    if v1 is None or v2 is None:
+        return ''
+    return v1 * v2
+
+@register.filter
+def divide(value, arg):
+    v1 = parse_decimal(value)
+    v2 = parse_decimal(arg)
+    if v1 is None or v2 is None:
+        return ''
+    try:
+        return v1 / v2
+    except ZeroDivisionError:
+        return ''
-- 
2.20.1

