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

---
 tests/test_formdata.py            | 82 ++++++++++++++++++++++++++++++-
 wcs/qommon/templatetags/qommon.py | 56 +++++++++++++++++++++
 2 files changed, 137 insertions(+), 1 deletion(-)

diff --git a/tests/test_formdata.py b/tests/test_formdata.py
index 3bb828a7..835e0782 100644
--- a/tests/test_formdata.py
+++ b/tests/test_formdata.py
@@ -7,7 +7,7 @@ import time
 
 from quixote import cleanup
 from quixote.http_request import Upload
-from qommon.template import Template
+from qommon.template import Template, TemplateError
 from qommon.form import PicklableUpload
 from wcs.qommon.http_request import HTTPRequest
 from wcs import fields, formdef
@@ -1336,3 +1336,83 @@ def test_lazy_formdata_decimal_filter(pub):
 
         tmpl = Template('{{ 4.12|decimal:form_var_arg }}')
         assert tmpl.render(context) == '4.120'
+
+def test_mathematics_filters(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):
+            # string as param
+            assert WorkflowStatusItem.compute('{{ "1,1"|add:0 }}', raises=True) == '1.1'
+            assert WorkflowStatusItem.compute('{{ "not a number"|add:1.2 }}', raises=True) == '1.2'
+
+            # 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'
+
+            # add
+            assert WorkflowStatusItem.compute('{{ 4|add:-0.9 }}', raises=True) == '3.1'
+            assert WorkflowStatusItem.compute('{{ 3.2|add:"" }}', raises=True) == '3.2'
+            assert WorkflowStatusItem.compute('{{ ""|add:3.3 }}', raises=True) == '3.3'
+            assert WorkflowStatusItem.compute('{{ 0|add:0 }}', raises=True) == '0'
+            assert WorkflowStatusItem.compute('{{ ""|add:0 }}', raises=True) == '0'
+            assert WorkflowStatusItem.compute('{{ 0|add:"" }}', raises=True) == '0'
+
+            # subtract
+            assert WorkflowStatusItem.compute('{{ 5|subtract:0.9 }}', raises=True) == '4.1'
+            assert WorkflowStatusItem.compute('{{ 4.2|subtract:"" }}', raises=True) == '4.2'
+            assert WorkflowStatusItem.compute('{{ ""|subtract:-4.3 }}', raises=True) == '4.3'
+
+            # multiply
+            assert WorkflowStatusItem.compute('{{ 2.5|multiply:2 }}', raises=True) == '5.0'
+            assert WorkflowStatusItem.compute('{{ "3"|multiply:"2" }}', raises=True) == '6'
+            assert WorkflowStatusItem.compute('{{ 5|multiply:"" }}', raises=True) == ''
+            assert WorkflowStatusItem.compute('{{ ""|multiply:"5.2"}}', raises=True) == ''
+
+            # divide
+            assert WorkflowStatusItem.compute('{{ 2|divide:3|decimal:2 }}', raises=True) == '0.67'
+            assert WorkflowStatusItem.compute('{{ 6|divide:"" }}', raises=True) == ''
+            assert WorkflowStatusItem.compute('{{ ""|divide:6}}', raises=True) == ''
+
+            # no arg
+            with pytest.raises(Exception, match=r'.*Could not find variable at start.*'):
+                WorkflowStatusItem.compute('{{ |add:2}}', 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)
+
+def test_lazy_formdata_mathematics_filters(pub):
+    formdef = FormDef()
+    formdef.name = 'foobar'
+    formdef.url_name = 'foobar'
+    formdef.fields = [
+        fields.StringField(id='0', label='term1', varname='term1'),
+        fields.StringField(id='1', label='term2', varname='term2')
+    ]
+    formdef.store()
+    formdata = formdef.data_class()()
+    formdata.data = {'0': '3', '1': '4'}
+    formdata.store()
+    pub.substitutions.feed(formdata)
+    for mode in (None, 'lazy'):
+        context = pub.substitutions.get_context_variables(mode=mode)
+
+        tmpl = Template('{{ form_var_term1|decimal }}')
+        assert tmpl.render(context) == '3'
+
+        tmpl = Template('{{ form_var_term1|add:form_var_term2 }}')
+        assert tmpl.render(context) == '7'
+
+        tmpl = Template('{{ form_var_term1|subtract:form_var_term2 }}')
+        assert tmpl.render(context) == '-1'
+
+        tmpl = Template('{{ form_var_term1|multiply:form_var_term2 }}')
+        assert tmpl.render(context) == '12'
+
+        tmpl = Template('{{ form_var_term1|divide:form_var_term2 }}')
+        assert tmpl.render(context) == '0.75'
diff --git a/wcs/qommon/templatetags/qommon.py b/wcs/qommon/templatetags/qommon.py
index ceafe7fb..34588b1b 100644
--- a/wcs/qommon/templatetags/qommon.py
+++ b/wcs/qommon/templatetags/qommon.py
@@ -229,3 +229,59 @@ def action_button(context, action_id, label, delay=3):
     }
     token.store()
     return '---===BUTTON:%s:%s===---' % (token.id, label)
+
+@register.filter
+def add(value, arg):
+    '''replace the "add" native django filter'''
+    if hasattr(value, 'get_value'):
+        value = value.get_value()  # unlazy
+    if hasattr(arg, 'get_value'):
+        arg = arg.get_value()  # unlazy
+    value = parse_decimal(value)
+    arg = parse_decimal(arg)
+    if value is None:
+        value = Decimal(0)
+    if arg is None:
+        arg = Decimal(0)
+    return value + arg
+
+@register.filter
+def subtract(value, arg):
+    if hasattr(value, 'get_value'):
+        value = value.get_value()  # unlazy
+    if hasattr(arg, 'get_value'):
+        arg = arg.get_value()  # unlazy
+    value = parse_decimal(value)
+    arg = parse_decimal(arg)
+    if value is None:
+        value = Decimal(0)
+    if arg is None:
+        arg = Decimal(0)
+    return value - arg
+
+@register.filter
+def multiply(value, arg):
+    if hasattr(value, 'get_value'):
+        value = value.get_value()  # unlazy
+    if hasattr(arg, 'get_value'):
+        arg = arg.get_value()  # unlazy
+    value = parse_decimal(value)
+    arg = parse_decimal(arg)
+    if value is None or arg is None:
+        return ''
+    return value * arg
+
+@register.filter
+def divide(value, arg):
+    if hasattr(value, 'get_value'):
+        value = value.get_value()  # unlazy
+    if hasattr(arg, 'get_value'):
+        arg = arg.get_value()  # unlazy
+    value = parse_decimal(value)
+    arg = parse_decimal(arg)
+    if value is None or arg is None:
+        return ''
+    try:
+        return value / arg
+    except ZeroDivisionError:
+        return ''
-- 
2.20.1

