From d647b6102a4e9f87be0840ddaa822cdcef456ba4 Mon Sep 17 00:00:00 2001 From: Thomas NOEL Date: Mon, 2 Oct 2017 22:57:23 +0200 Subject: [PATCH] admin: add a python expression widged, use it in page conditions (#19113) --- tests/test_api.py | 17 ++++++++++++++++ tests/test_widgets.py | 26 ++++++++++++++++++++++++ wcs/api.py | 24 +++++++++++++++++++++-- wcs/fields.py | 4 ++-- wcs/qommon/form.py | 20 ++++++++++++++++++- wcs/qommon/static/css/dc2/admin.css | 38 ++++++++++++++++++++++++------------ wcs/qommon/static/js/qommon.admin.js | 2 +- 7 files changed, 112 insertions(+), 19 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 43ac6367..b10fb5b9 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1806,6 +1806,23 @@ def test_validate_expression(pub): resp = get_app(pub).get('/api/validate-expression?expression==hello[\'plop\']') assert resp.json == {'klass': None, 'msg': ''} +def test_validate_python_expression(pub): + resp = get_app(pub).get('/api/validate-python-expression?expression=hello') + assert resp.json == {'klass': None, 'msg': ''} + resp = get_app(pub).get('/api/validate-python-expression?expression=form_var_x == "2"') + assert resp.json == {'klass': None, 'msg': ''} + resp = get_app(pub).get('/api/validate-python-expression?expression=hello[\'plop\']') + assert resp.json == {'klass': None, 'msg': ''} + resp = get_app(pub).get('/api/validate-python-expression?expression=[hello]') + assert resp.json['klass'] == 'warning' + assert resp.json['msg'].startswith('Make sure you use a Python expression') + resp = get_app(pub).get('/api/validate-python-expression?expression==hello') + assert resp.json['klass'] == 'error' + assert resp.json['msg'] == 'Python expression can not start with a = sign.' + resp = get_app(pub).get('/api/validate-python-expression?expression=[if-any]coucou[end]') + assert resp.json['klass'] == 'error' + assert resp.json['msg'].startswith('Python syntax error') + @pytest.fixture(params=['sql', 'pickle']) def no_request_pub(request): pub = create_temporary_pub(sql_mode=bool(request.param == 'sql')) diff --git a/tests/test_widgets.py b/tests/test_widgets.py index 10997b75..7a1e2f9c 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -424,6 +424,32 @@ def test_computed_expression_widget(): assert widget.has_error() assert widget.get_error().startswith('error in template') +def test_python_expression_widget(): + widget = PythonExpressionWidget('test') + form = MockHtmlForm(widget) + mock_form_submission(req, widget, {'test': '"hello world"'}) + assert widget.parse() == '"hello world"' + assert not widget.has_error() + + widget = PythonExpressionWidget('test') + mock_form_submission(req, widget, {'test': 'hello world'}) + assert widget.has_error() + assert widget.get_error().startswith('Python syntax error') + + widget = PythonExpressionWidget('test') + mock_form_submission(req, widget, {'test': 'form_var_xxx'}) + assert not widget.has_error() + + widget = PythonExpressionWidget('test') + mock_form_submission(req, widget, {'test': '=form_var_xxx'}) + assert widget.has_error() + assert widget.get_error().startswith('Python syntax error') + + widget = PythonExpressionWidget('test') + mock_form_submission(req, widget, {'test': 'blouf=3'}) + assert widget.has_error() + assert widget.get_error().startswith('Python syntax error') + def test_wcsextrastringwidget(): widget = WcsExtraStringWidget('test', value='foo', required=True) mock_form_submission(req, widget, {'test': ''}) diff --git a/wcs/api.py b/wcs/api.py index 2ab8fa5a..6e6e257a 100644 --- a/wcs/api.py +++ b/wcs/api.py @@ -27,7 +27,7 @@ from qommon import _ from qommon import misc from qommon.errors import (AccessForbiddenError, QueryError, TraversalError, UnknownNameIdAccessForbiddenError) -from qommon.form import ValidationError, ComputedExpressionWidget +from qommon.form import ValidationError, ComputedExpressionWidget, PythonExpressionWidget from wcs.categories import Category from wcs.formdef import FormDef @@ -644,7 +644,8 @@ class ApiTrackingCodeDirectory(Directory): class ApiDirectory(Directory): _q_exports = ['forms', 'roles', ('reverse-geocoding', 'reverse_geocoding'), 'formdefs', 'categories', 'user', 'users', 'code', - ('validate-expression', 'validate_expression'),] + ('validate-expression', 'validate_expression'), + ('validate-python-expression', 'validate_python_expression'),] forms = ApiFormsDirectory() formdefs = ApiFormdefsDirectory() @@ -699,6 +700,25 @@ class ApiDirectory(Directory): hint['msg'] = _('Make sure you want a Python expression, not a simple template string.') return json.dumps(hint) + def validate_python_expression(self): + get_response().set_content_type('application/json') + expression = get_request().form.get('expression') + hint = {'klass': None, 'msg': ''} + if expression and expression.startswith('='): # usual error + hint['klass'] = 'error' + hint['msg'] = _('Python expression can not start with a = sign.') + else: + try: + PythonExpressionWidget.validate(expression) + except ValidationError as e: + hint['klass'] = 'error' + hint['msg'] = str(e) + else: + if expression and re.match(r'^.*\[[a-zA-Z_]\w*\]', expression): + hint['klass'] = 'warning' + hint['msg'] = _('Make sure you use a Python expression and not a template string.') + return json.dumps(hint) + def _q_traverse(self, path): get_request().is_json_marker = True return super(ApiDirectory, self)._q_traverse(path) diff --git a/wcs/fields.py b/wcs/fields.py index 4f8f436f..00c74d51 100644 --- a/wcs/fields.py +++ b/wcs/fields.py @@ -1405,7 +1405,7 @@ class PostConditionsRowWidget(CompositeWidget): CompositeWidget.__init__(self, name, value, **kwargs) if not value: value = {} - self.add(StringWidget, name='condition', title=_('Condition'), + self.add(PythonExpressionWidget, name='condition', title=_('Condition'), value=value.get('condition'), size=50) self.add(StringWidget, name='error_message', title=_('Error message if condition is not met'), value=value.get('error_message'), size=50) @@ -1468,7 +1468,7 @@ class PageField(Field): def fill_admin_form(self, form): form.add(StringWidget, 'label', title = _('Label'), value = self.label, required = True, size = 50) - form.add(StringWidget, 'condition', title = _('Condition'), value = self.condition, + form.add(PythonExpressionWidget, 'condition', title = _('Condition'), value = self.condition, required = False, size = 50) form.add(PostConditionsTableWidget, 'post_conditions', title=_('Post Conditions'), diff --git a/wcs/qommon/form.py b/wcs/qommon/form.py index 1cb6be35..dd7e7d1e 100644 --- a/wcs/qommon/form.py +++ b/wcs/qommon/form.py @@ -2222,8 +2222,11 @@ class ComputedExpressionWidget(StringWidget): '''StringWidget that checks the entered value is a correct workflow expression.''' + extra_css_class = 'validate-expression-widget' + validation_url = 'api/validate-expression' + def render_content(self): - validation_url = get_publisher().get_root_url() + 'api/validate-expression' + validation_url = get_publisher().get_root_url() + self.validation_url self.attrs['data-validation-url'] = validation_url return StringWidget.render_content(self) @@ -2270,3 +2273,18 @@ class ComputedExpressionWidget(StringWidget): self.validate(self.value) except ValidationError as e: self.set_error(str(e)) + + +class PythonExpressionWidget(ComputedExpressionWidget): + '''StringWidget that checks the entered value is a correct Python expression.''' + + validation_url = 'api/validate-python-expression' + + @classmethod + def validate(cls, expression): + if not expression: + return + try: + compile(expression, '', 'eval') + except SyntaxError as e: + raise ValidationError(_('Python syntax error (%s)') % e) diff --git a/wcs/qommon/static/css/dc2/admin.css b/wcs/qommon/static/css/dc2/admin.css index 8e33e40c..c4bfc4ce 100644 --- a/wcs/qommon/static/css/dc2/admin.css +++ b/wcs/qommon/static/css/dc2/admin.css @@ -1291,31 +1291,24 @@ form table div.widget { margin-bottom: 1ex; } -div.ComputedExpressionWidget div.content { +div.validate-expression-widget div.content { position: relative; } -div.ComputedExpressionWidget div.content input, -div.ComputedExpressionWidget div.content input:focus { - border-left-width: 3ex; -} - -div.ComputedExpressionWidget.hint-error div.content input { +div.validate-expression-widget.hint-error div.content input { border-color: red; } -div.ComputedExpressionWidget.hint-warning div.content input { +div.validate-expression-widget.hint-warning div.content input { border-color: orange; } -div.ComputedExpressionWidget.hint-error div.content span.hint-text, -div.ComputedExpressionWidget.hint-warning div.content span.hint-text { +div.validate-expression-widget.hint-error div.content span.hint-text, +div.validate-expression-widget.hint-warning div.content span.hint-text { display: block; } -div.ComputedExpressionWidget div.content::before { - font-family: FontAwesome; - content: "\f1b2"; +div.validate-expression-widget div.content::before { position: absolute; top: 8px; left: 6px; @@ -1323,6 +1316,25 @@ div.ComputedExpressionWidget div.content::before { border: 1px solid transparent; } +div.ComputedExpressionWidget div.content input, +div.ComputedExpressionWidget div.content input:focus { + border-left-width: 3ex; +} + +div.ComputedExpressionWidget div.content::before { + font-family: FontAwesome; + content: "\f1b2"; +} + +div.PythonExpressionWidget div.content input, +div.PythonExpressionWidget div.content input:focus { + border-left-width: 7ex; +} + +div.PythonExpressionWidget div.content::before { + content: "Python"; +} + div.admin-permissions thead th { transform: rotate(-45deg); transform-origin: 10% 0; diff --git a/wcs/qommon/static/js/qommon.admin.js b/wcs/qommon/static/js/qommon.admin.js index eda24dfd..bb2feef6 100644 --- a/wcs/qommon/static/js/qommon.admin.js +++ b/wcs/qommon/static/js/qommon.admin.js @@ -41,7 +41,7 @@ $(function() { var validation_timeout_id = 0; $('input[data-validation-url]').on('change focus keyup', function() { var val = $(this).val(); - var $widget = $(this).parents('.ComputedExpressionWidget'); + var $widget = $(this).parents('.validate-expression-widget'); var validation_url = $(this).data('validation-url'); clearTimeout(validation_timeout_id); validation_timeout_id = setTimeout(function() { -- 2.14.2