forms: evaluate conditions during typing (#22102)
This commit is contained in:
parent
602597edd9
commit
62267edcfa
|
@ -2015,6 +2015,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_condition(pub):
|
||||
resp = get_app(pub).get('/api/validate-condition?type=python&value_python=hello')
|
||||
assert resp.json == {'klass': None, 'msg': ''}
|
||||
resp = get_app(pub).get('/api/validate-condition?type=python&value_python=~2')
|
||||
assert resp.json == {'klass': None, 'msg': ''}
|
||||
resp = get_app(pub).get('/api/validate-condition?type=python&value_python=hello -')
|
||||
assert resp.json['klass'] == 'error'
|
||||
assert resp.json['msg'].startswith('syntax error')
|
||||
|
||||
resp = get_app(pub).get('/api/validate-condition?type=django&value_django=~2')
|
||||
assert resp.json['klass'] == 'error'
|
||||
assert resp.json['msg'].startswith('syntax error')
|
||||
|
||||
resp = get_app(pub).get('/api/validate-condition?type=unknown&value_unknown=2')
|
||||
assert resp.json['klass'] == 'error'
|
||||
assert resp.json['msg'] == 'unknown condition type'
|
||||
|
||||
@pytest.fixture(params=['sql', 'pickle'])
|
||||
def no_request_pub(request):
|
||||
pub = create_temporary_pub(sql_mode=bool(request.param == 'sql'))
|
||||
|
|
19
wcs/api.py
19
wcs/api.py
|
@ -27,9 +27,10 @@ from qommon import misc
|
|||
from qommon.evalutils import make_datetime
|
||||
from qommon.errors import (AccessForbiddenError, QueryError, TraversalError,
|
||||
UnknownNameIdAccessForbiddenError)
|
||||
from qommon.form import ValidationError, ComputedExpressionWidget
|
||||
from qommon.form import ComputedExpressionWidget, ConditionWidget
|
||||
|
||||
from wcs.categories import Category
|
||||
from wcs.conditions import Condition, ValidationError
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.roles import Role, logged_users_role
|
||||
from wcs.forms.common import FormStatusPage
|
||||
|
@ -668,7 +669,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-condition', 'validate_condition')]
|
||||
|
||||
forms = ApiFormsDirectory()
|
||||
formdefs = ApiFormdefsDirectory()
|
||||
|
@ -720,6 +722,19 @@ class ApiDirectory(Directory):
|
|||
hint['msg'] = _('Make sure you want a Python expression, not a simple template string.')
|
||||
return json.dumps(hint)
|
||||
|
||||
def validate_condition(self):
|
||||
get_response().set_content_type('application/json')
|
||||
condition = {}
|
||||
condition['type'] = get_request().form.get('type') or ''
|
||||
condition['value'] = get_request().form.get('value_' + condition['type'])
|
||||
hint = {'klass': None, 'msg': ''}
|
||||
try:
|
||||
Condition(condition).validate()
|
||||
except ValidationError as e:
|
||||
hint['klass'] = 'error'
|
||||
hint['msg'] = str(e)
|
||||
return json.dumps(hint)
|
||||
|
||||
def _q_traverse(self, path):
|
||||
get_request().is_json_marker = True
|
||||
return super(ApiDirectory, self)._q_traverse(path)
|
||||
|
|
|
@ -17,9 +17,13 @@
|
|||
import sys
|
||||
|
||||
from quixote import get_publisher
|
||||
from django.template import Context, Template
|
||||
from django.template import Context, Template, TemplateSyntaxError
|
||||
|
||||
from qommon import get_logger
|
||||
from qommon import _, get_logger
|
||||
|
||||
|
||||
class ValidationError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class Condition(object):
|
||||
|
@ -60,3 +64,22 @@ class Condition(object):
|
|||
template = Template('{%% load qommon %%}{%% if %s %%}OK{%% endif %%}' % self.value)
|
||||
context = Context(local_variables)
|
||||
return template.render(context) == 'OK'
|
||||
|
||||
def validate(self):
|
||||
if not self.type or not self.value:
|
||||
return
|
||||
if not hasattr(self, 'validate_' + self.type):
|
||||
raise ValidationError(_('unknown condition type'))
|
||||
return getattr(self, 'validate_' + self.type)()
|
||||
|
||||
def validate_python(self):
|
||||
try:
|
||||
compile(self.value, '<string>', 'eval')
|
||||
except (SyntaxError, TypeError) as e:
|
||||
raise ValidationError(_('syntax error: %s') % e)
|
||||
|
||||
def validate_django(self):
|
||||
try:
|
||||
Template('{%% load qommon %%}{%% if %s %%}OK{%% endif %%}' % self.value)
|
||||
except TemplateSyntaxError as e:
|
||||
raise ValidationError(_('syntax error: %s') % e)
|
||||
|
|
|
@ -66,6 +66,7 @@ from django.utils.safestring import mark_safe
|
|||
|
||||
from .template import render as render_template, Template, TemplateError
|
||||
from wcs.portfolio import has_portfolio
|
||||
from wcs.conditions import Condition, ValidationError
|
||||
|
||||
from qommon import _, ngettext
|
||||
import misc
|
||||
|
@ -195,9 +196,6 @@ class SubmitWidget(quixote.form.widget.SubmitWidget):
|
|||
return htmltag('button', name=self.name, value=value, **self.attrs) + \
|
||||
self.label + htmltext('</button>')
|
||||
|
||||
class ValidationError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class RadiobuttonsWidget(quixote.form.RadiobuttonsWidget):
|
||||
def __init__(self, name, value=None, **kwargs):
|
||||
|
@ -454,10 +452,14 @@ class CompositeWidget(quixote.form.CompositeWidget):
|
|||
r = TemplateIO(html=True)
|
||||
r += htmltext('<tr>\n')
|
||||
for widget in self.get_widgets():
|
||||
extra_attributes = ''
|
||||
classnames = '%s widget' % widget.__class__.__name__
|
||||
if hasattr(widget, 'extra_css_class') and widget.extra_css_class:
|
||||
classnames += ' ' + widget.extra_css_class
|
||||
r += htmltext('<td><div class="%s"><div class="content">' % classnames)
|
||||
if hasattr(widget, 'content_extra_attributes'):
|
||||
extra_attributes = ' '.join(['%s=%s' % x for x in widget.content_extra_attributes.items()])
|
||||
r += htmltext('<td><div class="%s"><div class="content" %s>' % (
|
||||
classnames, extra_attributes))
|
||||
r += widget.render_content()
|
||||
r += htmltext('</div></div></td>')
|
||||
r += htmltext('</tr>\n')
|
||||
|
@ -2305,6 +2307,11 @@ class ConditionWidget(CompositeWidget):
|
|||
attrs={'data-dynamic-display-child-of': '%s$type' % self.name,
|
||||
'data-dynamic-display-value': 'python'})
|
||||
|
||||
@property
|
||||
def content_extra_attributes(self):
|
||||
validation_url = get_publisher().get_root_url() + 'api/validate-condition'
|
||||
return {'data-validation-url': validation_url}
|
||||
|
||||
def _parse(self, request):
|
||||
self.value = {}
|
||||
if not self.get('type') or self.get('type') == 'none':
|
||||
|
@ -2315,11 +2322,10 @@ class ConditionWidget(CompositeWidget):
|
|||
if self.value['type']:
|
||||
self.value['value'] = self.get('value_%s' % self.value['type'])
|
||||
|
||||
if self.value['type'] == 'python' and self.value.get('value'):
|
||||
try:
|
||||
compile(self.value.get('value'), '<string>', 'eval')
|
||||
except (SyntaxError, TypeError), e:
|
||||
self.set_error(_('invalid expression: %s') % e)
|
||||
try:
|
||||
Condition(self.value).validate()
|
||||
except ValidationError as e:
|
||||
self.set_error(str(e))
|
||||
|
||||
if not self.value['value']:
|
||||
self.value = None
|
||||
|
|
|
@ -1302,8 +1302,11 @@ div.ComputedExpressionWidget div.content input:focus {
|
|||
border-left-width: 3ex;
|
||||
}
|
||||
|
||||
div.ComputedExpressionWidget.hint-error div.content input {
|
||||
div.content.hint-error input[type],
|
||||
.hint-error div.content input[type] {
|
||||
border-color: red;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 1px red;
|
||||
}
|
||||
|
||||
div.ComputedExpressionWidget.hint-warning div.content input {
|
||||
|
|
|
@ -63,6 +63,34 @@ $(function() {
|
|||
return false;
|
||||
});
|
||||
|
||||
$('div[data-validation-url]').each(function(idx, elem) {
|
||||
var $widget = $(this);
|
||||
var widget_name = $widget.find('select').attr('name');
|
||||
var prefix = widget_name.replace(/\$[a-z]*/, '') + '$';
|
||||
$(this).find('input, select').on('change focus keyup', function() {
|
||||
clearTimeout($widget.validation_timeout_id);
|
||||
$widget.validation_timeout_id = setTimeout(function() {
|
||||
var data = Object();
|
||||
$widget.find('select, input').each(function(idx, elem) {
|
||||
data[$(elem).attr('name').replace(prefix, '')] = $(elem).val();
|
||||
});
|
||||
$.ajax({
|
||||
url: $widget.data('validation-url'),
|
||||
data: data,
|
||||
dataType: 'json',
|
||||
success: function(data) {
|
||||
$widget.removeClass('hint-warning');
|
||||
$widget.removeClass('hint-error');
|
||||
if (data.klass) {
|
||||
$widget.addClass('hint-' + data.klass);
|
||||
}
|
||||
$widget.prop('title', data.msg);
|
||||
}
|
||||
})}, 250);
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
/* keep title/slug in sync */
|
||||
$('body').delegate('input[data-slug-sync]', 'keyup change paste',
|
||||
function() {
|
||||
|
|
|
@ -19,7 +19,11 @@
|
|||
{{widget.rendered_title}}
|
||||
{% endblock %}
|
||||
{% block widget-content %}
|
||||
<div class="content {{widget.content.content_extra_css_class}}">
|
||||
<div class="content {{widget.content.content_extra_css_class}}"
|
||||
{% for attr, value in widget.content_extra_attributes.items %}
|
||||
{{attr}}="{{value}}"
|
||||
{% endfor %}
|
||||
>
|
||||
{% block widget-error %}{{widget.rendered_error}}{% endblock %}
|
||||
{% block widget-control %}{{widget.render_content|safe}}{% endblock %}
|
||||
{% block widget-hint %}{{widget.rendered_hint}}{% endblock %}
|
||||
|
|
Loading…
Reference in New Issue