From d41461a8274cf3003d9630f16b767029c2ad3d2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= Date: Wed, 25 May 2016 10:43:05 +0200 Subject: [PATCH] misc: add new widget to validate workflow expressions (#11042) --- tests/test_widgets.py | 21 +++++++++++++++++ wcs/qommon/form.py | 45 +++++++++++++++++++++++++++++++++++++ wcs/qommon/static/css/dc2/admin.css | 1 + wcs/wf/geolocate.py | 8 +++---- wcs/wf/jump.py | 2 +- wcs/wf/wscall.py | 3 ++- wcs/workflows.py | 27 ++-------------------- 7 files changed, 76 insertions(+), 31 deletions(-) diff --git a/tests/test_widgets.py b/tests/test_widgets.py index 4a93aa0..2ee1971 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -382,3 +382,24 @@ def test_select_or_other_widget(): options=[('apple', 'Apple'), ('pear', 'Pear'), ('peach', 'Peach')]) mock_form_submission(req, widget, {'test$choice': ['__other'], 'test$other': 'Apricot'}) assert widget.parse() == 'Apricot' + +def test_computed_expression_widget(): + widget = ComputedExpressionWidget('test') + form = MockHtmlForm(widget) + mock_form_submission(req, widget, {'test': 'hello world'}) + assert widget.parse() == 'hello world' + assert not widget.has_error() + + widget = ComputedExpressionWidget('test') + mock_form_submission(req, widget, {'test': '=hello world'}) + assert widget.has_error() + assert widget.get_error().startswith('syntax error') + + widget = ComputedExpressionWidget('test') + mock_form_submission(req, widget, {'test': '[form_var_xxx]'}) + assert not widget.has_error() + + widget = ComputedExpressionWidget('test') + mock_form_submission(req, widget, {'test': '[end]'}) + assert widget.has_error() + assert widget.get_error().startswith('error in template') diff --git a/wcs/qommon/form.py b/wcs/qommon/form.py index 2c4d802..a84f722 100644 --- a/wcs/qommon/form.py +++ b/wcs/qommon/form.py @@ -59,6 +59,7 @@ import misc from strftime import strftime from publisher import get_cfg from wcs import file_validation +from . import ezt QuixoteForm = Form @@ -2234,3 +2235,47 @@ class SingleSelectWidgetWithOther(CompositeWidget): self.value = self.get('choice') if self.value == '__other': self.value = self.get('other') + + +class ComputedExpressionWidget(StringWidget): + '''StringWidget that checks the entered value is a correct workflow + expression.''' + + @classmethod + def validate_ezt(cls, template): + processor = ezt.Template(compress_whitespace=False) + try: + processor.parse(template or '') + except ezt.EZTException as e: + parts = [] + parts.append({ + ezt.ArgCountSyntaxError: _('wrong number of arguments'), + ezt.UnknownReference: _('unknown reference'), + ezt.NeedSequenceError: _('sequence required'), + ezt.UnclosedBlocksError: _('unclosed block'), + ezt.UnmatchedEndError: _('unmatched [end]'), + ezt.BaseUnavailableError: _('unavailable base location'), + ezt.BadFormatConstantError: _('bad format constant'), + ezt.UnknownFormatConstantError: _('unknown format constant'), + }.get(e.__class__)) + if e.line: + parts.append(_('at line %(line)d and column %(column)d') % { + 'line': e.line+1, + 'column': e.column+1}) + raise ValueError(_('error in template (%s)') % ' '.join(parts)) + + def _parse(self, request): + StringWidget._parse(self, request) + if self.value: + if self.value.startswith('='): + # python expression + try: + compile(self.value[1:], '', 'eval') + except SyntaxError as e: + self.set_error(_('syntax error (%s)') % e) + else: + # ezt expression + try: + self.validate_ezt(self.value) + except ValueError as e: + self.set_error(str(e)) diff --git a/wcs/qommon/static/css/dc2/admin.css b/wcs/qommon/static/css/dc2/admin.css index 3b9fcb7..03936a2 100644 --- a/wcs/qommon/static/css/dc2/admin.css +++ b/wcs/qommon/static/css/dc2/admin.css @@ -1170,6 +1170,7 @@ div.WidgetDict div.content div.StringWidget { width: 25%; } +div.WidgetDict div.content div + div.ComputedExpressionWidget, div.WidgetDict div.content div + div.StringWidget { width: 70%; padding-left: 1em; diff --git a/wcs/wf/geolocate.py b/wcs/wf/geolocate.py index 534fe3a..cc9d9fa 100644 --- a/wcs/wf/geolocate.py +++ b/wcs/wf/geolocate.py @@ -27,7 +27,7 @@ except ImportError: from quixote import get_publisher from qommon import get_logger -from qommon.form import RadiobuttonsWidget, StringWidget, CheckboxWidget +from qommon.form import RadiobuttonsWidget, ComputedExpressionWidget, CheckboxWidget from qommon.misc import http_get_page from wcs.workflows import WorkflowStatusItem, register_item_class @@ -60,21 +60,21 @@ class GeolocateWorkflowStatusItem(WorkflowStatusItem): value=self.method, attrs={'data-dynamic-display-parent': 'true'}) if 'address_string' in parameters: - form.add(StringWidget, '%saddress_string' % prefix, size=50, + form.add(ComputedExpressionWidget, '%saddress_string' % prefix, size=50, title=_('Address String'), value=self.address_string, attrs={ 'data-dynamic-display-child-of': '%smethod' % prefix, 'data-dynamic-display-value': methods.get('address_string'), }) if 'map_variable' in parameters: - form.add(StringWidget, '%smap_variable' % prefix, size=50, + form.add(ComputedExpressionWidget, '%smap_variable' % prefix, size=50, title=_('Map Variable'), value=self.map_variable, attrs={ 'data-dynamic-display-child-of': '%smethod' % prefix, 'data-dynamic-display-value': methods.get('map_variable'), }) if 'photo_variable' in parameters: - form.add(StringWidget, '%sphoto_variable' % prefix, size=50, + form.add(ComputedExpressionWidget, '%sphoto_variable' % prefix, size=50, title=_('Photo Variable'), value=self.photo_variable, attrs={ 'data-dynamic-display-child-of': '%smethod' % prefix, diff --git a/wcs/wf/jump.py b/wcs/wf/jump.py index 3b376ea..d426050 100644 --- a/wcs/wf/jump.py +++ b/wcs/wf/jump.py @@ -166,7 +166,7 @@ class JumpWorkflowStatusItem(WorkflowStatusJumpItem): 'variables': ', '.join(timewords()), 'granularity': seconds2humanduration(self._granularity)} if str(self.timeout).startswith('='): - form.add(StringWidget, '%stimeout' % prefix, title=_('Timeout'), + form.add(ComputedExpressionWidget, '%stimeout' % prefix, title=_('Timeout'), value=self.timeout, hint=_hint) else: form.add(StringWidget, '%stimeout' % prefix, title=_('Timeout'), diff --git a/wcs/wf/wscall.py b/wcs/wf/wscall.py index 8d46bce..1765210 100644 --- a/wcs/wf/wscall.py +++ b/wcs/wf/wscall.py @@ -147,7 +147,7 @@ class WebserviceCallStatusItem(WorkflowStatusItem): title=_('URL'), value=self.url, size=80, hint=_('Common substitution variables are available with the [variable] syntax.')) if 'request_signature_key' in parameters: - form.add(StringWidget, '%srequest_signature_key' % prefix, + form.add(ComputedExpressionWidget, '%srequest_signature_key' % prefix, title=_('Request Signature Key'), value=self.request_signature_key) methods = collections.OrderedDict( @@ -170,6 +170,7 @@ class WebserviceCallStatusItem(WorkflowStatusItem): form.add(WidgetDict, '%spost_data' % prefix, title=_('Post data'), value=self.post_data or {}, + element_value_type=ComputedExpressionWidget, attrs={ 'data-dynamic-display-child-of': '%smethod' % prefix, 'data-dynamic-display-value': methods.get('POST'), diff --git a/wcs/workflows.py b/wcs/workflows.py index 3635a58..fccbcb8 100644 --- a/wcs/workflows.py +++ b/wcs/workflows.py @@ -1872,15 +1872,14 @@ class SendmailWorkflowStatusItem(WorkflowStatusItem): self.get_list_of_roles(include_logged_in_users=False)}) if 'subject' in parameters: form.add(StringWidget, '%ssubject' % prefix, title=_('Subject'), - validation_function=validate_ezt, + validation_function=ComputedExpressionWidget.validate_ezt, value=self.subject, size=40) if 'body' in parameters: form.add(TextWidget, '%sbody' % prefix, title=_('Body'), value=self.body, cols=80, rows=10, - validation_function=validate_ezt, + validation_function=ComputedExpressionWidget.validate_ezt, hint=_('Available variables: url, url_status, details, name, number, comment, field_NAME')) - def perform(self, formdata): if not self.to: return @@ -2006,28 +2005,6 @@ def template_on_formdata(formdata=None, template=None, process=None, return fd.getvalue() -def validate_ezt(template): - processor = ezt.Template(compress_whitespace=False) - try: - processor.parse(template or '') - except ezt.EZTException as e: - parts = [] - parts.append({ - ezt.ArgCountSyntaxError: _('wrong number of arguments'), - ezt.UnknownReference: _('unknown reference'), - ezt.NeedSequenceError: _('sequence required'), - ezt.UnclosedBlocksError: _('unclosed block'), - ezt.UnmatchedEndError: _('unmatched [end]'), - ezt.BaseUnavailableError: _('unavailable base location'), - ezt.BadFormatConstantError: _('bad format constant'), - ezt.UnknownFormatConstantError: _('unknown format constant'), - }.get(e.__class__)) - if e.line: - parts.append(_('at line %(line)d and column %(column)d') % { - 'line': e.line+1, - 'column': e.column+1}) - raise ValueError(_('error in template (%s)') % ' '.join(parts)) - class SendSMSWorkflowStatusItem(WorkflowStatusItem): description = N_('Send SMS') key = 'sendsms' -- 2.8.1