From da32fc5effd021a1fdfa47f2af93965e30581b61 Mon Sep 17 00:00:00 2001
From: Thomas NOEL <tnoel@entrouvert.com>
Date: Wed, 6 Feb 2019 00:11:01 +0100
Subject: [PATCH] add test tools in formdata inspect view (#23685)

---
 tests/test_backoffice_pages.py      |  78 ++++++++++++++++++++
 wcs/backoffice/management.py        | 108 +++++++++++++++++++++++++++-
 wcs/conditions.py                   |   7 +-
 wcs/qommon/static/css/dc2/admin.css |  23 ++++++
 4 files changed, 212 insertions(+), 4 deletions(-)

diff --git a/tests/test_backoffice_pages.py b/tests/test_backoffice_pages.py
index 43ee5c0c..609e1ffe 100644
--- a/tests/test_backoffice_pages.py
+++ b/tests/test_backoffice_pages.py
@@ -3727,6 +3727,84 @@ def test_inspect_page(pub, local_user):
     resp = login(get_app(pub)).get('%sinspect' % formdata.get_url(backoffice=True), status=200)
     assert re.findall('New Function.*(deleted)', resp.body)
 
+    # test tools
+    resp = login(get_app(pub)).get('%sinspect' % formdata.get_url(backoffice=True), status=200)
+    assert "Test tool" in resp.body
+
+    resp.form['test_mode'] = 'condition'
+
+    resp.form['condition$type'] = 'python'
+    resp.form['condition$value_python'] = 'len(form_name) == 10'  # "form title"
+    resp = resp.form.submit()
+    assert 'Condition result' in resp.body
+    assert 'result-true' in resp.body
+    assert '<code>True</code>' in resp.body
+    resp.form['condition$value_python'] = 'len(form_name) == 5'
+    resp = resp.form.submit()
+    assert 'Condition result' in resp.body
+    assert 'result-false' in resp.body
+    assert '<code>False</code>' in resp.body
+    resp.form['condition$value_python'] = 'form_do_not_exist == 3'
+    resp = resp.form.submit()
+    assert 'Condition result' not in resp.body
+    assert 'Failed to evaluate condition' in resp.body
+    assert 'NameError' in resp.body
+
+    resp.form['condition$type'] = 'django'
+    resp.form['condition$value_django'] = 'form_name|length == 10'
+    resp = resp.form.submit()
+    assert 'Condition result' in resp.body
+    assert 'result-true' in resp.body
+    resp.form['condition$type'] = 'django'
+    resp.form['condition$value_django'] = 'form_name|length == 5'
+    resp = resp.form.submit()
+    assert 'Condition result' in resp.body
+    assert 'result-false' in resp.body
+    resp.form['condition$type'] = 'django'
+    resp.form['condition$value_django'] = 'foo bar'
+    resp = resp.form.submit()
+    assert 'Condition result' not in resp.body
+    assert 'syntax error' in resp.body
+    resp.form['condition$value_python'] = None
+    resp.form['condition$value_django'] = None
+
+    resp.form['test_mode'] = 'template'
+    resp.form['template'] = '{{ form_name }}'
+    resp = resp.form.submit()
+    assert 'Template rendering' in resp.body
+    assert '<div class="testtool-result">form title</div>' in resp.body
+    assert 'HTML Source' not in resp.body
+    resp.form['template'] = '<p>{{ form_name }}</p>'
+    resp = resp.form.submit()
+    assert 'Template rendering' in resp.body
+    assert '<p>form title</p>' in resp.body
+    assert 'HTML Source' in resp.body
+    resp.form['template'] = '{% for x in 1 %}ok{% endfor %}'
+    resp = resp.form.submit()
+    assert 'Failed to evaluate template' in resp.body
+    assert 'TypeError' in resp.body
+    resp.form['template'] = '{% for x in 1 %}ok{% end %}'
+    resp = resp.form.submit()
+    assert 'syntax error' in resp.body
+    assert 'Invalid block tag' in resp.body
+    resp.form['template'] = ''
+
+    resp.form['test_mode'] = 'html_template'
+    resp.form['html_template'] = '<p>{{ form_name }}</p>'
+    resp = resp.form.submit()
+    resp = resp.form.submit()
+    assert 'Template rendering' in resp.body
+    assert '<p>form title</p>' in resp.body
+    assert 'HTML Source' in resp.body
+    resp.form['html_template'] = '{% for x in 1 %}ok{% endfor %}'
+    resp = resp.form.submit()
+    assert 'Failed to evaluate HTML template' in resp.body
+    assert 'TypeError' in resp.body
+    resp.form['html_template'] = '{% for x in 1 %}ok{% end %}'
+    resp = resp.form.submit()
+    assert 'syntax error' in resp.body
+    assert 'Invalid block tag' in resp.body
+
 def test_workflow_jump_previous(pub):
     user = create_user(pub)
     create_environment(pub)
diff --git a/wcs/backoffice/management.py b/wcs/backoffice/management.py
index 1a7cefe3..b8ef8980 100644
--- a/wcs/backoffice/management.py
+++ b/wcs/backoffice/management.py
@@ -32,7 +32,7 @@ from quixote import get_session, get_publisher, get_request, get_response, redir
 from quixote.directory import Directory
 from quixote.html import TemplateIO, htmltext, htmlescape
 
-from qommon import _, ngettext
+from qommon import _, ngettext, ezt
 from qommon.admin.emails import EmailsDirectory
 from qommon.admin.menu import command_icon
 from qommon.backoffice.menu import html_top
@@ -50,6 +50,7 @@ from qommon.storage import (Equal, NotEqual, LessOrEqual, GreaterOrEqual, Or,
         Intersects, ILike, FtsMatch, Contains, Null)
 
 from wcs.api_utils import get_user_from_api_query_string
+from wcs.conditions import Condition
 from wcs.forms.backoffice import FormDefUI
 from wcs.forms.common import FormStatusPage
 from wcs.admin.settings import UserFieldsFormDef
@@ -57,7 +58,7 @@ from wcs.categories import Category
 from wcs.formdata import FormData
 from wcs.formdef import FormDef
 from wcs.roles import logged_users_role, Role
-from wcs.workflows import get_role_translation
+from wcs.workflows import get_role_translation, template_on_formdata
 
 from .submission import FormFillPage
 
@@ -2237,6 +2238,109 @@ class FormBackOfficeStatusPage(FormStatusPage):
                     self.formdef.workflow.id, _('Workflow Definition'))
         r += htmltext('</span>')
         r += htmltext('</div>')
+
+        form = Form(use_tokens=False)
+        form.add(SingleSelectWidget, 'test_mode',
+                 options=[
+                     (None, ('Choose a testing tool'), None),
+                     ('condition', _('Condition'), 'condition'),
+                     ('template', '%s / %s' % (_('Template'), _('Django Expression')), 'template'),
+                     ('html_template', _('HTML Template (wysiwyg)'), 'html_template'),
+                     # TODO: ODT/RTF tester
+                 ], attrs={'data-dynamic-display-parent': 'true'})
+        form.add(ConditionWidget, 'condition',
+                 attrs={
+                     'data-dynamic-display-child-of': 'test_mode',
+                     'data-dynamic-display-value': 'condition'
+                 })
+        form.add(WysiwygTextWidget, 'html_template',
+                 validation_function=ComputedExpressionWidget.validate_template,
+                 attrs={
+                     'data-dynamic-display-child-of': 'test_mode',
+                     'data-dynamic-display-value': 'html_template'
+                 })
+        form.add(TextWidget, 'template',
+                 validation_function=ComputedExpressionWidget.validate_template,
+                 attrs={
+                     'data-dynamic-display-child-of': 'test_mode',
+                     'data-dynamic-display-value': 'template'
+                 })
+        form.add_submit('submit', _('Process'),
+                        attrs={
+                            'data-dynamic-display-child-of': 'test_mode',
+                            'data-dynamic-display-value-in': 'condition|html_template|template'
+                        })
+        r += htmltext('<div id="tester" class="section foldable %s">') % (
+            'folded' if not form.is_submitted() else '')
+        r += htmltext('<h2>Test tool</h2>')
+        r += htmltext('<div>')
+        r += form.render()
+
+        if form.is_submitted() and not form.has_errors():
+            # show test result
+            test_mode = form.get_widget('test_mode').parse()
+            if test_mode == 'condition':
+                condition = Condition(form.get_widget('condition').parse())
+                condition.log_errors = False
+                condition.record_errors = False
+                try:
+                    result = condition.unsafe_evaluate()
+                except Exception as exception:
+                    r += htmltext('<div class="errornotice">')
+                    r += htmltext('<p>%s</p>') % _('Failed to evaluate condition')
+                    r += htmltext('<p>%s <code>%s: %s</code></p>') % (
+                        _('Error message:'), exception.__class__.__name__, str(exception))
+                    r += htmltext('</div>')
+                else:
+                    r += htmltext('<h3>%s</h3>') % _('Condition result')
+                    r += htmltext('<div class="testtool-result">')
+                    r += htmltext('<p><span class="result-%s">%s</span>') % (
+                            str(bool(result)).lower(), ('True') if result else _('False'))
+                    if condition.type == 'python':
+                        r += htmltext(' &mdash; %s <strong><code>%r</code></strong> '
+                                      '<span class="type">(%r)</span>') % (
+                                        _('Python actual result is'), result, type(result))
+                    r += htmltext('</p>')
+                    r += htmltext('</div>')
+            elif test_mode == 'template':
+                try:
+                    template = form.get_widget('template').parse() or ''
+                    result = template_on_formdata(self.filled, template, raises=True)
+                except Exception as exception:
+                    r += htmltext('<div class="errornotice">')
+                    r += htmltext('<p>%s</p>') % _('Failed to evaluate template')
+                    r += htmltext('<p>%s <code>%s: %s</code></p>') % (
+                            _('Error message:'), exception.__class__.__name__, str(exception))
+                else:
+                    r += htmltext('<h3>%s</h3>') % _('Template rendering')
+                    if result and result[0] == '<':  # seems to be HTML
+                        r += htmltext('<div class="testtool-result">')
+                        r += htmltext(result)
+                        r += htmltext('</div>')
+                        r += htmltext('<h3>%s</h3>') % _('HTML Source')
+                        r += htmltext('<pre class="testtool-result">%s</pre>') % result
+                    else:
+                        r += htmltext('<div class="testtool-result">%s</div>') % result
+            elif test_mode == 'html_template':
+                try:
+                    html_template = form.get_widget('html_template').parse() or ''
+                    result = template_on_formdata(self.filled, html_template, raises=True,
+                                                  ezt_format=ezt.FORMAT_HTML)
+                except Exception as exception:
+                    r += htmltext('<div class="errornotice">')
+                    r += htmltext('<p>%s</p>') % _('Failed to evaluate HTML template')
+                    r += htmltext('<p>%s <code>%s: %s</code></p>') % (
+                            _('Error message:'), exception.__class__.__name__, str(exception))
+                    r += htmltext('</div>')
+                else:
+                    r += htmltext('<h3>%s</h3>') % _('Template rendering')
+                    r += htmltext('<div class="testtool-result">')
+                    r += htmltext(result)
+                    r += htmltext('</div>')
+                    r += htmltext('<h3>%s</h3>') % _('HTML Source')
+                    r += htmltext('<pre class="testtool-result">%s</pre>') % result
+        r += htmltext('</div></div>')
+
         r += htmltext('<ul class="biglist form-inspector">')
         r += htmltext(' <li><h3>%s</h3></li>') % _('Substitution variables')
         substvars = self.filled.get_static_substitution_variables()
diff --git a/wcs/conditions.py b/wcs/conditions.py
index bbe37804..89727c18 100644
--- a/wcs/conditions.py
+++ b/wcs/conditions.py
@@ -45,12 +45,15 @@ class Condition(object):
         return get_publisher().substitutions.get_context_variables(
                 mode='%s-condition' % self.type)
 
-    def evaluate(self):
+    def unsafe_evaluate(self):
         if not self.type or not self.value:
             return True
         local_variables = self.get_data()
+        return getattr(self, 'evaluate_' + self.type)(local_variables)
+
+    def evaluate(self):
         try:
-            return getattr(self, 'evaluate_' + self.type)(local_variables)
+            return self.unsafe_evaluate()
         except Exception as e:
             if self.log_errors:
                 get_logger().warn('failed to evaluate %r (%r)', self, e)
diff --git a/wcs/qommon/static/css/dc2/admin.css b/wcs/qommon/static/css/dc2/admin.css
index 00948fbd..e6b63453 100644
--- a/wcs/qommon/static/css/dc2/admin.css
+++ b/wcs/qommon/static/css/dc2/admin.css
@@ -1567,6 +1567,29 @@ ul.form-inspector li div.value span.type {
 	position: absolute;
 }
 
+div.testtool-result, pre.testtool-result {
+	padding: 0.6em;
+	background: #ddd;
+	border: 1px solid #888;
+}
+
+div.testtool-result span.type {
+	font-size: 80%;
+	color: #888;
+	padding-left: 1em;
+}
+
+div.testtool-result span.result-true {
+	padding: 5px;
+	background: lightgreen;
+}
+
+div.testtool-result span.result-false {
+	padding: 5px;
+	background: red;
+	color: white;
+}
+
 ul#evolutions li div.msg li {
 	margin: 1em 0;
 }
-- 
2.20.1

