From 8b4c87861cd212cfb06b7785ed21b583533e14dc Mon Sep 17 00:00:00 2001
From: Nicolas ROCHE <nroche@entrouvert.com>
Date: Mon, 25 Feb 2019 10:58:55 +0100
Subject: [PATCH] templatetags: add mathematics filters (#27709)

---
 tests/test_formdata.py            | 77 +++++++++++++++++++++++++++++++
 tests/test_templates.py           | 64 +++++++++++++++++++++++++
 wcs/qommon/templatetags/qommon.py | 24 ++++++++++
 3 files changed, 165 insertions(+)

diff --git a/tests/test_formdata.py b/tests/test_formdata.py
index 3bb828a7..1319b5ff 100644
--- a/tests/test_formdata.py
+++ b/tests/test_formdata.py
@@ -583,6 +583,8 @@ def variable_test_data(pub):
         fields.MapField(id='7', label='map', varname='map'),
         fields.DateField(id='8', label='date2', varname='datefield2'),
         fields.StringField(id='9', label='string2', varname='datestring'),
+        fields.StringField(id='10', label='number1', varname='term1'),
+        fields.StringField(id='11', label='number2', varname='term2'),
     ]
     formdef.workflow_roles = {'_receiver': role.id}
     formdef.geolocations = {'base': 'Base'}
@@ -603,6 +605,8 @@ def variable_test_data(pub):
         '7': '2;4',  # map
         '8': time.strptime('2018-08-31', '%Y-%m-%d'),
         '9': '2018-07-31',
+        '10': '3',
+        '11': '4'
     }
     formdata.data['5'].receive(['hello world'])
     formdata.geolocations = {'base': {'lat': 1, 'lon': 2}}
@@ -1336,3 +1340,76 @@ def test_lazy_formdata_decimal_filter(pub):
 
         tmpl = Template('{{ 4.12|decimal:form_var_arg }}')
         assert tmpl.render(context) == '4.120'
+
+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'
+
+def test_mathematic_conditions_django(pub, variable_test_data):
+    for true_condition_value in (
+            # reminder
+            'form_var_term1 == 3',
+            'form_var_term2 == 4',
+
+            # add
+            'form_var_term1|add:form_var_term2 == 7',
+            'form_var_term1|add:form_var_term2 == 7.0',
+            'form_var_term1|add:form_var_term2 == "7"|decimal',
+            'form_var_term1|add:form_var_term2 > 6',
+
+            # subtract
+            'form_var_term1|subtract:form_var_term2 == -1',
+            'form_var_term1|subtract:form_var_term2 == -1.0',
+            'form_var_term1|subtract:form_var_term2 == "-1"|decimal',
+            'form_var_term1|subtract:form_var_term2 < 0',
+
+            # multiply
+            'form_var_term1|multiply:form_var_term2 == 12',
+            'form_var_term1|multiply:form_var_term2 == 12.0',
+            'form_var_term1|multiply:form_var_term2 == "12"|decimal',
+            'form_var_term1|multiply:form_var_term2 > 10',
+
+            # divide
+            'form_var_term1|divide:form_var_term2 == 0.75',
+            'form_var_term1|divide:form_var_term2 == 0.750',
+            'form_var_term1|divide:form_var_term2 == "0.75"|decimal',
+            'form_var_term1|divide:form_var_term2 > 0.5'
+            ):
+        condition = Condition({'type': 'django', 'value': true_condition_value})
+        assert condition.evaluate() is True
+
+    for false_condition_value in (
+            'form_var_term1|add:form_var_term2 > 8',
+            'form_var_term1|subtract:form_var_term2 > 0',
+            'form_var_term1|multiply:form_var_term2 > 20',
+            'form_var_term1|divide:form_var_term2 > 1',
+            ):
+        condition = Condition({'type': 'django', 'value': false_condition_value})
+        assert condition.evaluate() is False
diff --git a/tests/test_templates.py b/tests/test_templates.py
index 24a5d411..cb5f66a6 100644
--- a/tests/test_templates.py
+++ b/tests/test_templates.py
@@ -323,3 +323,67 @@ def test_decimal_templatetag():
     assert tmpl.render() == 'hello'
     tmpl = Template('{% if 3|decimal|decimal == 3 %}hello{% endif %}')
     assert tmpl.render() == 'hello'
+
+def test_mathematics_templatetag():
+    tmpl = Template('{{ term1|add:term2 }}')
+
+    # using string: convert to number or 0
+    assert tmpl.render({'term1': '1.1', 'term2': 0}) == '1.1'
+    assert tmpl.render({'term1': 'not a number', 'term2': 1.2}) == '1.2'
+    assert tmpl.render({'term1': 0.3, 'term2': "1"}) == '1.3'
+    assert tmpl.render({'term1': 1.4, 'term2': "not a number"}) == '1.4'
+
+    # add
+    assert tmpl.render({'term1': 4, 'term2': -0.9}) == '3.1'
+    assert tmpl.render({'term1': '4', 'term2': -0.8}) == '3.2'
+    assert tmpl.render({'term1': 4, 'term2': '-0.7'}) == '3.3'
+    assert tmpl.render({'term1': '4', 'term2': '-0.6'}) == '3.4'
+    assert tmpl.render({'term1': '', 'term2': 3.5}) == '3.5'
+    assert tmpl.render({'term1': 3.6, 'term2': ''}) == '3.6'
+    assert tmpl.render({'term1': '', 'term2': ''}) == '0'
+    assert tmpl.render({'term1': 0, 'term2': ''}) == '0'
+    assert tmpl.render({'term1': '', 'term2': 0}) == '0'
+    assert tmpl.render({'term1': 0, 'term2': 0}) == '0'
+
+    # subtract
+    tmpl = Template('{{ term1|subtract:term2 }}')
+    assert tmpl.render({'term1': 5.1, 'term2': 1}) == '4.1'
+    assert tmpl.render({'term1': '5.2', 'term2': 1}) == '4.2'
+    assert tmpl.render({'term1': 5.3, 'term2': '1'}) == '4.3'
+    assert tmpl.render({'term1': '5.4', 'term2': '1'}) == '4.4'
+    assert tmpl.render({'term1': '', 'term2': -4.5}) == '4.5'
+    assert tmpl.render({'term1': 4.6, 'term2': ''}) == '4.6'
+    assert tmpl.render({'term1': '', 'term2': ''}) == '0'
+    assert tmpl.render({'term1': 0, 'term2': ''}) == '0'
+    assert tmpl.render({'term1': '', 'term2': 0}) == '0'
+    assert tmpl.render({'term1': 0, 'term2': 0}) == '0'
+
+    # multiply
+    tmpl = Template('{{ term1|multiply:term2 }}')
+    assert tmpl.render({'term1': '3', 'term2': '2'}) == '6'
+    assert tmpl.render({'term1': 2.5, 'term2': 2}) == '5.0'
+    assert tmpl.render({'term1': '2.5', 'term2': 2}) == '5.0'
+    assert tmpl.render({'term1': 2.5, 'term2': '2'}) == '5.0'
+    assert tmpl.render({'term1': '2.5', 'term2': '2'}) == '5.0'
+    assert tmpl.render({'term1': '', 'term2': '2'}) == '0'
+    assert tmpl.render({'term1': 2.5, 'term2': ''}) == '0.0'
+    assert tmpl.render({'term1': '', 'term2': ''}) == '0'
+    assert tmpl.render({'term1': 0, 'term2': ''}) == '0'
+    assert tmpl.render({'term1': '', 'term2': 0}) == '0'
+    assert tmpl.render({'term1': 0, 'term2': 0}) == '0'
+
+    # divide
+    tmpl = Template('{{ term1|divide:term2 }}')
+    assert tmpl.render({'term1': 16, 'term2': 2}) == '8'
+    assert tmpl.render({'term1': 6, 'term2': 0.75}) == '8'
+    assert tmpl.render({'term1': '6', 'term2': 0.75}) == '8'
+    assert tmpl.render({'term1': 6, 'term2': '0.75'}) == '8'
+    assert tmpl.render({'term1': '6', 'term2': '0.75'}) == '8'
+    assert tmpl.render({'term1': '', 'term2': '2'}) == '0'
+    assert tmpl.render({'term1': 6, 'term2': ''}) == ''
+    assert tmpl.render({'term1': '', 'term2': ''}) == ''
+    assert tmpl.render({'term1': 0, 'term2': ''}) == ''
+    assert tmpl.render({'term1': '', 'term2': 0}) == ''
+    assert tmpl.render({'term1': 0, 'term2': 0}) == ''
+    tmpl = Template('{{ term1|divide:term2|decimal:2 }}')
+    assert tmpl.render({'term1': 2, 'term2': 3}) == '0.67'
diff --git a/wcs/qommon/templatetags/qommon.py b/wcs/qommon/templatetags/qommon.py
index 12c8684a..5abad7f1 100644
--- a/wcs/qommon/templatetags/qommon.py
+++ b/wcs/qommon/templatetags/qommon.py
@@ -16,6 +16,8 @@
 
 import datetime
 from decimal import Decimal
+from decimal import InvalidOperation as DecimalInvalidOperation
+from decimal import DivisionByZero as DecimalDivisionByZero
 
 from django import template
 from django.template import defaultfilters
@@ -227,3 +229,25 @@ 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'''
+    return parse_decimal(term1) + parse_decimal(term2)
+
+@register.filter
+def subtract(term1, term2):
+    return parse_decimal(term1) - parse_decimal(term2)
+
+@register.filter
+def multiply(term1, term2):
+    return parse_decimal(term1) * parse_decimal(term2)
+
+@register.filter
+def divide(term1, term2):
+    try:
+        return parse_decimal(term1) / parse_decimal(term2)
+    except DecimalInvalidOperation:
+        return ''
+    except DecimalDivisionByZero:
+        return ''
-- 
2.20.1

