0001-admin-add-live-validation-hints-to-computed-expressi.patch
tests/test_api.py | ||
---|---|---|
1485 | 1485 |
formdata.remove_self() |
1486 | 1486 |
resp = get_app(pub).get('/api/code/%s' % code.id, status=404) |
1487 | 1487 | |
1488 |
def test_validate_expression(pub): |
|
1489 |
resp = get_app(pub).get('/api/validate-expression?expression=hello') |
|
1490 |
assert resp.json == {'klass': None, 'msg': ''} |
|
1491 |
resp = get_app(pub).get('/api/validate-expression?expression=[hello]') |
|
1492 |
assert resp.json == {'klass': None, 'msg': ''} |
|
1493 |
resp = get_app(pub).get('/api/validate-expression?expression==[hello') |
|
1494 |
assert resp.json['klass'] == 'error' |
|
1495 |
assert resp.json['msg'].startswith('syntax error') |
|
1496 |
resp = get_app(pub).get('/api/validate-expression?expression==[hello]') |
|
1497 |
assert resp.json['klass'] == 'warning' |
|
1498 |
assert resp.json['msg'].startswith('Make sure you want a Python expression,') |
|
1499 | ||
1488 | 1500 |
@pytest.fixture(params=['sql', 'pickle']) |
1489 | 1501 |
def no_request_pub(request): |
1490 | 1502 |
pub = create_temporary_pub(sql_mode=bool(request.param == 'sql')) |
tests/test_widgets.py | ||
---|---|---|
21 | 21 |
global pub, req |
22 | 22 |
pub = create_temporary_pub() |
23 | 23 | |
24 |
req = HTTPRequest(None, {}) |
|
24 |
req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'})
|
|
25 | 25 |
req.language = None |
26 | 26 |
pub._set_request(req) |
27 | 27 |
wcs/api.py | ||
---|---|---|
16 | 16 | |
17 | 17 |
import hashlib |
18 | 18 |
import json |
19 |
import re |
|
19 | 20 |
import time |
20 | 21 |
import urllib2 |
21 | 22 |
import sys |
... | ... | |
25 | 26 |
from qommon import misc |
26 | 27 |
from qommon.errors import (AccessForbiddenError, QueryError, TraversalError, |
27 | 28 |
UnknownNameIdAccessForbiddenError) |
29 |
from qommon.form import ValidationError, ComputedExpressionWidget |
|
28 | 30 | |
29 | 31 |
from wcs.categories import Category |
30 | 32 |
from wcs.data_sources import NamedDataSource |
... | ... | |
611 | 613 |
class ApiDirectory(Directory): |
612 | 614 |
_q_exports = ['forms', 'roles', ('reverse-geocoding', 'reverse_geocoding'), |
613 | 615 |
'formdefs', 'categories', 'user', 'users', 'code', |
614 |
'datasources'] |
|
616 |
'datasources', ('validate-expression', 'validate_expression'),]
|
|
615 | 617 | |
616 | 618 |
forms = ApiFormsDirectory() |
617 | 619 |
formdefs = ApiFormdefsDirectory() |
... | ... | |
648 | 650 |
get_response().set_content_type('application/json') |
649 | 651 |
return json.dumps({'data': list_roles}) |
650 | 652 | |
653 |
def validate_expression(self): |
|
654 |
get_response().set_content_type('application/json') |
|
655 |
expression = get_request().form.get('expression') |
|
656 |
hint = {'klass': None, 'msg': ''} |
|
657 |
try: |
|
658 |
ComputedExpressionWidget.validate(expression) |
|
659 |
except ValidationError as e: |
|
660 |
hint['klass'] = 'error' |
|
661 |
hint['msg'] = str(e) |
|
662 |
else: |
|
663 |
if expression and re.match(r'^=.*\[\w+\]', expression): |
|
664 |
hint['klass'] = 'warning' |
|
665 |
hint['msg'] = _('Make sure you want a Python expression, not a simple template string.') |
|
666 |
return json.dumps(hint) |
|
667 | ||
651 | 668 |
def _q_traverse(self, path): |
652 | 669 |
get_request().is_json_marker = True |
653 | 670 |
return super(ApiDirectory, self)._q_traverse(path) |
wcs/qommon/form.py | ||
---|---|---|
200 | 200 |
SubmitWidget.render_content = submit_render_content |
201 | 201 | |
202 | 202 | |
203 |
class ValidationError(ValueError): |
|
204 |
pass |
|
205 | ||
206 | ||
203 | 207 |
class RadiobuttonsWidget(quixote.form.RadiobuttonsWidget): |
204 | 208 |
def __init__(self, name, value=None, **kwargs): |
205 | 209 |
self.options_with_attributes = kwargs.pop('options_with_attributes', None) |
... | ... | |
2303 | 2307 |
'''StringWidget that checks the entered value is a correct workflow |
2304 | 2308 |
expression.''' |
2305 | 2309 | |
2310 |
def render_content(self): |
|
2311 |
validation_url = get_publisher().get_root_url() + 'api/validate-expression' |
|
2312 |
self.attrs['data-validation-url'] = validation_url |
|
2313 |
return StringWidget.render_content(self) |
|
2314 | ||
2306 | 2315 |
@classmethod |
2307 | 2316 |
def validate_ezt(cls, template): |
2308 | 2317 |
processor = ezt.Template(compress_whitespace=False) |
... | ... | |
2325 | 2334 |
parts.append(_('at line %(line)d and column %(column)d') % { |
2326 | 2335 |
'line': e.line+1, |
2327 | 2336 |
'column': e.column+1}) |
2328 |
raise ValueError(_('error in template (%s)') % ' '.join(parts)) |
|
2337 |
raise ValidationError(_('error in template (%s)') % ' '.join(parts)) |
|
2338 | ||
2339 |
@classmethod |
|
2340 |
def validate(cls, expression): |
|
2341 |
if not expression: |
|
2342 |
return |
|
2343 |
if expression.startswith('=') and len(expression) > 1: |
|
2344 |
try: |
|
2345 |
compile(expression[1:], '<string>', 'eval') |
|
2346 |
except SyntaxError as e: |
|
2347 |
raise ValidationError(_('syntax error (%s)') % e) |
|
2348 |
else: |
|
2349 |
cls.validate_ezt(expression) |
|
2329 | 2350 | |
2330 | 2351 |
def _parse(self, request): |
2331 | 2352 |
StringWidget._parse(self, request) |
2332 | 2353 |
if self.value: |
2333 |
if self.value.startswith('='): |
|
2334 |
# python expression |
|
2335 |
try: |
|
2336 |
compile(self.value[1:], '<string>', 'eval') |
|
2337 |
except SyntaxError as e: |
|
2338 |
self.set_error(_('syntax error (%s)') % e) |
|
2339 |
else: |
|
2340 |
# ezt expression |
|
2341 |
try: |
|
2342 |
self.validate_ezt(self.value) |
|
2343 |
except ValueError as e: |
|
2344 |
self.set_error(str(e)) |
|
2354 |
try: |
|
2355 |
self.validate(self.value) |
|
2356 |
except ValidationError as e: |
|
2357 |
self.set_error(str(e)) |
wcs/qommon/static/css/dc2/admin.css | ||
---|---|---|
1253 | 1253 |
border-left-width: 3ex; |
1254 | 1254 |
} |
1255 | 1255 | |
1256 |
div.ComputedExpressionWidget.hint-error div.content input { |
|
1257 |
border-color: red; |
|
1258 |
} |
|
1259 | ||
1260 |
div.ComputedExpressionWidget.hint-warning div.content input { |
|
1261 |
border-color: orange; |
|
1262 |
} |
|
1263 | ||
1264 |
div.ComputedExpressionWidget.hint-error div.content span.hint-text, |
|
1265 |
div.ComputedExpressionWidget.hint-warning div.content span.hint-text { |
|
1266 |
display: block; |
|
1267 |
} |
|
1268 | ||
1256 | 1269 |
div.ComputedExpressionWidget div.content::before { |
1257 | 1270 |
font-family: FontAwesome; |
1258 | 1271 |
content: "\f1b2"; |
wcs/qommon/static/js/qommon.admin.js | ||
---|---|---|
37 | 37 |
}); |
38 | 38 |
}); |
39 | 39 | |
40 |
/* hints on the computed expression widget */ |
|
41 |
var validation_timeout_id = 0; |
|
42 |
$('input[data-validation-url]').on('change focus keyup', function() { |
|
43 |
var val = $(this).val(); |
|
44 |
var $widget = $(this).parents('.ComputedExpressionWidget'); |
|
45 |
var validation_url = $(this).data('validation-url'); |
|
46 |
clearTimeout(validation_timeout_id); |
|
47 |
validation_timeout_id = setTimeout(function() { |
|
48 |
$.ajax({ |
|
49 |
url: validation_url, |
|
50 |
data: {expression: val}, |
|
51 |
dataType: 'json', |
|
52 |
success: function(data) { |
|
53 |
$widget.removeClass('hint-warning'); |
|
54 |
$widget.removeClass('hint-error'); |
|
55 |
if (data.klass) { |
|
56 |
$widget.addClass('hint-' + data.klass); |
|
57 |
} |
|
58 |
$widget.prop('title', data.msg); |
|
59 |
} |
|
60 |
})}, 250); |
|
61 |
return false; |
|
62 |
}); |
|
63 | ||
40 | 64 |
/* keep sidebar sticky */ |
41 | 65 |
if ($('#sidebar').length) { |
42 | 66 |
var $window = $(window); |
43 |
- |