From 58fca067e59a766bdb997147adf39d3e5ae48454 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            | 109 +++++++++++++++++++++++++++++-
 wcs/qommon/templatetags/qommon.py |  58 +++++++++++++++-
 2 files changed, 165 insertions(+), 2 deletions(-)

diff --git a/tests/test_formdata.py b/tests/test_formdata.py
index 3bb828a7..d9099279 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,110 @@ 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('{{ "4"|add:-0.8 }}', raises=True) == '3.2'
+            assert WorkflowStatusItem.compute('{{ 4|add:"-0.7" }}', raises=True) == '3.3'
+            assert WorkflowStatusItem.compute('{{ "4"|add:"-0.6" }}', raises=True) == '3.4'
+            assert WorkflowStatusItem.compute('{{ ""|add:3.5 }}', raises=True) == '3.5'
+            assert WorkflowStatusItem.compute('{{ 3.6|add:"" }}', raises=True) == '3.6'
+            assert WorkflowStatusItem.compute('{{ ""|add:"" }}', raises=True) == '0'
+            assert WorkflowStatusItem.compute('{{ 0|add:"" }}', raises=True) == '0'
+            assert WorkflowStatusItem.compute('{{ ""|add:0 }}', raises=True) == '0'
+            assert WorkflowStatusItem.compute('{{ 0|add:0 }}', raises=True) == '0'
+
+            # subtract
+            assert WorkflowStatusItem.compute('{{ 5.1|subtract:1 }}', raises=True) == '4.1'
+            assert WorkflowStatusItem.compute('{{ "5.2"|subtract:1 }}', raises=True) == '4.2'
+            assert WorkflowStatusItem.compute('{{ 5.3|subtract:"1" }}', raises=True) == '4.3'
+            assert WorkflowStatusItem.compute('{{ "5.4"|subtract:"1" }}', raises=True) == '4.4'
+            assert WorkflowStatusItem.compute('{{ ""|subtract:-4.5 }}', raises=True) == '4.5'
+            assert WorkflowStatusItem.compute('{{ 4.6|subtract:"" }}', raises=True) == '4.6'
+            assert WorkflowStatusItem.compute('{{ ""|subtract:"" }}', raises=True) == '0'
+            assert WorkflowStatusItem.compute('{{ 0|subtract:"" }}', raises=True) == '0'
+            assert WorkflowStatusItem.compute('{{ ""|subtract:0 }}', raises=True) == '0'
+            assert WorkflowStatusItem.compute('{{ 0|subtract:0 }}', raises=True) == '0'
+
+            # multiply
+            assert WorkflowStatusItem.compute('{{ "3"|multiply:"2" }}', raises=True) == '6'
+            assert WorkflowStatusItem.compute('{{ 2.5|multiply:2 }}', raises=True) == '5.0'
+            assert WorkflowStatusItem.compute('{{ "2.5"|multiply:2 }}', raises=True) == '5.0'
+            assert WorkflowStatusItem.compute('{{ 2.5|multiply:"2" }}', raises=True) == '5.0'
+            assert WorkflowStatusItem.compute('{{ "2.5"|multiply:"2" }}', raises=True) == '5.0'
+            assert WorkflowStatusItem.compute('{{ ""|multiply:"2" }}', raises=True) == ''
+            assert WorkflowStatusItem.compute('{{ 2.5|multiply:"" }}', raises=True) == ''
+            assert WorkflowStatusItem.compute('{{ ""|multiply:"" }}', raises=True) == ''
+            assert WorkflowStatusItem.compute('{{ 0|multiply:"" }}', raises=True) == ''
+            assert WorkflowStatusItem.compute('{{ ""|multiply:0 }}', raises=True) == ''
+            assert WorkflowStatusItem.compute('{{ 0|multiply:0 }}', raises=True) == '0'
+
+            # divide
+            assert WorkflowStatusItem.compute('{{ 16|divide:2 }}', raises=True) == '8'
+            assert WorkflowStatusItem.compute('{{ 2|divide:3|decimal:2 }}', raises=True) == '0.67'
+            assert WorkflowStatusItem.compute('{{ 6|divide:0.75 }}', raises=True) == '8'
+            assert WorkflowStatusItem.compute('{{ "6"|divide:0.75 }}', raises=True) == '8'
+            assert WorkflowStatusItem.compute('{{ 6|divide:"0.75" }}', raises=True) == '8'
+            assert WorkflowStatusItem.compute('{{ "6"|divide:"0.75" }}', raises=True) == '8'
+            assert WorkflowStatusItem.compute('{{ ""|divide:"2" }}', raises=True) == ''
+            assert WorkflowStatusItem.compute('{{ 6|divide:"" }}', raises=True) == ''
+            assert WorkflowStatusItem.compute('{{ ""|divide:"" }}', raises=True) == ''
+            assert WorkflowStatusItem.compute('{{ 0|divide:"" }}', raises=True) == ''
+            assert WorkflowStatusItem.compute('{{ ""|divide:0 }}', raises=True) == ''
+            assert WorkflowStatusItem.compute('{{ 0|divide:0 }}', 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..53b99683 100644
--- a/wcs/qommon/templatetags/qommon.py
+++ b/wcs/qommon/templatetags/qommon.py
@@ -15,7 +15,7 @@
 # along with this program; if not, see <http://www.gnu.org/licenses/>.
 
 import datetime
-from decimal import Decimal
+from decimal import Decimal, InvalidOperation as DecimalInvalidOperation
 
 from django import template
 from django.template import defaultfilters
@@ -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(term1, term2):
+    '''replace the "add" native django filter'''
+    if hasattr(term1, 'get_value'):
+        term1 = term1.get_value()  # unlazy
+    if hasattr(term2, 'get_value'):
+        term2 = term2.get_value()  # unlazy
+    term1 = parse_decimal(term1)
+    term2 = parse_decimal(term2)
+    if term1 is None:
+        term1 = Decimal(0)
+    if term2 is None:
+        term2 = Decimal(0)
+    return term1 + term2
+
+@register.filter
+def subtract(term1, term2):
+    if hasattr(term1, 'get_value'):
+        term1 = term1.get_value()  # unlazy
+    if hasattr(term2, 'get_value'):
+        term2 = term2.get_value()  # unlazy
+    term1 = parse_decimal(term1)
+    term2 = parse_decimal(term2)
+    if term1 is None:
+        term1 = Decimal(0)
+    if term2 is None:
+        term2 = Decimal(0)
+    return term1 - term2
+
+@register.filter
+def multiply(term1, term2):
+    if hasattr(term1, 'get_value'):
+        term1 = term1.get_value()  # unlazy
+    if hasattr(term2, 'get_value'):
+        term2 = term2.get_value()  # unlazy
+    term1 = parse_decimal(term1)
+    term2 = parse_decimal(term2)
+    if term1 is None or term2 is None:
+        return ''
+    return term1 * term2
+
+@register.filter
+def divide(term1, term2):
+    if hasattr(term1, 'get_value'):
+        term1 = term1.get_value()  # unlazy
+    if hasattr(term2, 'get_value'):
+        term2 = term2.get_value()  # unlazy
+    term1 = parse_decimal(term1)
+    term2 = parse_decimal(term2)
+    if term1 is None or term2 is None:
+        return ''
+    try:
+        return term1 / term2
+    except DecimalInvalidOperation:
+        return ''
-- 
2.20.1

