general: update UI of expression widget to match condition widget (#19112)

This commit is contained in:
Frédéric Péters 2018-05-28 14:39:59 +02:00
parent c6232cf715
commit bbe38a8fad
8 changed files with 146 additions and 59 deletions

View File

@ -2702,7 +2702,7 @@ def test_workflows_backoffice_fields(pub):
assert 'foobar3' not in options
resp.form['fields$element0$field_id'] = 'bo1'
resp.form['fields$element0$value'] = 'Hello'
resp.form['fields$element0$value$value_text'] = 'Hello'
resp = resp.form.submit('submit')
workflow = Workflow.get(workflow.id)
assert workflow.possible_status[0].items[0].fields == [{'field_id': 'bo1', 'value': 'Hello'}]

View File

@ -268,15 +268,18 @@ def test_fc_settings():
resp.forms[0]['scopes'].value = 'identite_pivot'
resp.forms[0]['user_field_mappings$element0$field_varname'] = 'prenoms'
resp.forms[0]['user_field_mappings$element0$value'] = '[given_name ""]'
resp.forms[0]['user_field_mappings$element0$value$value_template'] = '[given_name ""]'
resp.forms[0]['user_field_mappings$element0$value$type'] = 'template'
resp.forms[0]['user_field_mappings$element0$verified'] = 'Always'
resp.forms[0]['user_field_mappings$element1$field_varname'] = 'nom'
resp.forms[0]['user_field_mappings$element1$value'] = '[family_name ""]'
resp.forms[0]['user_field_mappings$element1$value$value_template'] = '[family_name ""]'
resp.forms[0]['user_field_mappings$element1$value$type'] = 'template'
resp.forms[0]['user_field_mappings$element1$verified'] = 'Always'
resp.forms[0]['user_field_mappings$element2$field_varname'] = 'email'
resp.forms[0]['user_field_mappings$element2$value'] = '[email ""]'
resp.forms[0]['user_field_mappings$element2$value$value_template'] = '[email ""]'
resp.forms[0]['user_field_mappings$element2$value$type'] = 'template'
resp.forms[0]['user_field_mappings$element2$verified'] = 'Always'
resp = resp.forms[0].submit('submit').follow()
@ -322,11 +325,13 @@ def test_fc_settings_no_user_profile():
resp.forms[0]['scopes'].value = 'identite_pivot'
resp.forms[0]['user_field_mappings$element0$field_varname'] = '__name'
resp.forms[0]['user_field_mappings$element0$value'] = '[given_name ""] [family_name ""]'
resp.forms[0]['user_field_mappings$element0$value$value_template'] = '[given_name ""] [family_name ""]'
resp.forms[0]['user_field_mappings$element0$value$type'] = 'template'
resp.forms[0]['user_field_mappings$element0$verified'] = 'Always'
resp.forms[0]['user_field_mappings$element2$field_varname'] = '__email'
resp.forms[0]['user_field_mappings$element2$value'] = '[email ""]'
resp.forms[0]['user_field_mappings$element2$value$value_template'] = '[email ""]'
resp.forms[0]['user_field_mappings$element2$value$type'] = 'template'
resp.forms[0]['user_field_mappings$element2$verified'] = 'Always'
resp = resp.forms[0].submit('submit').follow()

View File

@ -491,35 +491,35 @@ def test_composite_widget():
def test_computed_expression_widget():
widget = ComputedExpressionWidget('test')
form = MockHtmlForm(widget)
mock_form_submission(req, widget, {'test': 'hello world'})
mock_form_submission(req, widget, {'test$value_text': 'hello world', 'test$type': ['text']})
assert widget.parse() == 'hello world'
assert not widget.has_error()
widget = ComputedExpressionWidget('test')
mock_form_submission(req, widget, {'test': '=hello world'})
mock_form_submission(req, widget, {'test$value_python': '=hello world', 'test$type': ['python']})
assert widget.has_error()
assert widget.get_error().startswith('syntax error')
widget = ComputedExpressionWidget('test')
mock_form_submission(req, widget, {'test': '={{form_var_foo}}'})
mock_form_submission(req, widget, {'test$value_python': '{{form_var_foo}}', 'test$type': ['python']})
assert widget.has_error()
assert 'Python expression cannot contain {{' in widget.get_error()
widget = ComputedExpressionWidget('test')
mock_form_submission(req, widget, {'test': '{{ form_var_xxx }}'})
mock_form_submission(req, widget, {'test$value_template': '{{ form_var_xxx }}', 'test$type': ['template']})
assert not widget.has_error()
widget = ComputedExpressionWidget('test')
mock_form_submission(req, widget, {'test': '{% if True %}'})
mock_form_submission(req, widget, {'test$value_template': '{% if True %}', 'test$type': ['template']})
assert widget.has_error()
assert widget.get_error().startswith('syntax error in Django template')
widget = ComputedExpressionWidget('test')
mock_form_submission(req, widget, {'test': '[form_var_xxx]'})
mock_form_submission(req, widget, {'test$value_template': '[form_var_xxx]', 'test$type': ['template']})
assert not widget.has_error()
widget = ComputedExpressionWidget('test')
mock_form_submission(req, widget, {'test': '[end]'})
mock_form_submission(req, widget, {'test$value_template': '[end]', 'test$type': ['template']})
assert widget.has_error()
assert widget.get_error().startswith('syntax error in ezt template')

View File

@ -2230,14 +2230,69 @@ class SingleSelectWidgetWithOther(CompositeWidget):
self.value = self.get('other')
class ComputedExpressionWidget(StringWidget):
'''StringWidget that checks the entered value is a correct workflow
class ComputedExpressionWidget(CompositeWidget):
'''Widget that checks the entered value is a correct workflow
expression.'''
def __init__(self, name, value=None, *args, **kwargs):
if not value:
value = {}
else:
from wcs.workflows import WorkflowStatusItem
value = WorkflowStatusItem.get_expression(value)
CompositeWidget.__init__(self, name, value, **kwargs)
options = [
('text', _('Text'), 'text'),
('template', _('Template'), 'template'),
('python', _('Python Expression'), 'python'),
]
self.add(StringWidget, 'value_text', size=80,
value=value.get('value') if value.get('type') == 'text' else None)
self.add(StringWidget, 'value_template', size=80,
value=value.get('value') if value.get('type') == 'template' else None)
self.add(StringWidget, 'value_python', size=80,
value=value.get('value') if value.get('type') == 'python' else None)
self.add(SingleSelectWidget, 'type', options=options,
value=value.get('type'),
attrs={'data-dynamic-display-parent': 'true'})
def render_content(self):
validation_url = get_publisher().get_root_url() + 'api/validate-expression'
self.attrs['data-validation-url'] = validation_url
return StringWidget.render_content(self)
return htmltext('''
<style>
span[data-name="%(name)s"].text::after { content: "%(text_label)s"; }
span[data-name="%(name)s"].template::after { content: "%(template_label)s"; }
span[data-name="%(name)s"].python::after { content: "%(python_label)s"; }
</style>
<span class="text"
data-name="%(name)s"
data-dynamic-display-value="text"
data-dynamic-display-child-of="%(name)s$type"
>%(value_text)s</span
><span class="template"
data-name="%(name)s"
data-dynamic-display-value="template"
data-dynamic-display-child-of="%(name)s$type"
>%(value_template)s</span
><span class="python"
data-name="%(name)s"
data-dynamic-display-value="python"
data-dynamic-display-child-of="%(name)s$type"
>%(value_python)s</span
>%(type)s''') % {
'name': self.name,
'text_label': _('Text'),
'template_label': _('Template'),
'python_label': _('Python'),
'value_text': self.get_widget('value_text').render_content(),
'value_template': self.get_widget('value_template').render_content(),
'value_python': self.get_widget('value_python').render_content(),
'type': self.get_widget('type').render_content()
}
@classmethod
def validate_template(cls, template):
@ -2250,18 +2305,28 @@ class ComputedExpressionWidget(StringWidget):
def validate(cls, expression):
if not expression:
return
if expression.startswith('='):
if '{{' in expression[1:]:
from wcs.workflows import WorkflowStatusItem
expression = WorkflowStatusItem.get_expression(expression)
if expression['type'] == 'python':
if '{{' in expression['value']:
raise ValidationError(_('invalid usage, Python expression cannot contain {{'))
try:
compile(expression[1:], '<string>', 'eval')
compile(expression['value'], '<string>', 'eval')
except SyntaxError as e:
raise ValidationError(_('syntax error in Python expression: %s') % e)
else:
cls.validate_template(expression)
cls.validate_template(expression['value'])
def _parse(self, request):
StringWidget._parse(self, request)
self.value = None
if not self.get('type'):
return
value_type = self.get('type')
value_content = self.get('value_%s' % value_type)
if value_type == 'python':
self.value = '=' + value_content
else:
self.value = value_content
if self.value:
try:
self.validate(self.value)

View File

@ -977,17 +977,20 @@ ul.biglist li p.commands span {
box-shadow: 0 2px 2px 0px #ddd;
}
div.ComputedExpressionWidget div.content input[type=text],
div.ConditionWidget div.content input[type=text] {
margin: 0;
padding-right: 10ex;
}
div.ComputedExpressionWidget div.content span,
div.ConditionWidget div.content span.django,
div.ConditionWidget div.content span.python {
position: relative;
z-index: 3;
}
div.ComputedExpressionWidget div.content span::after,
div.ConditionWidget div.content span.django::after,
div.ConditionWidget div.content span.python::after {
position: absolute;
@ -1006,6 +1009,7 @@ div.ConditionWidget div.content span.python::after {
content: "Python";
}
div.ComputedExpressionWidget div.content select,
div.ConditionWidget div.content select {
width: 2em;
padding: 4px 2em 4px 1ex;
@ -1016,6 +1020,7 @@ div.ConditionWidget div.content select {
position: relative;
}
div.ComputedExpressionWidget div.content select:focus,
div.ConditionWidget div.content select:focus {
outline: none;
z-index: 5;
@ -1326,24 +1331,18 @@ div.SetBackofficeFieldsTableWidget br {
display: none;
}
div.SetBackofficeFieldsTableWidget td select,
div.SetBackofficeFieldsTableWidget td input {
div.SetBackofficeFieldsTableWidget td select {
width: 100%;
}
div.SetBackofficeFieldsTableWidget td input {
width: calc(100% - 3rem);
}
form table div.widget {
margin-bottom: 1ex;
}
div.ComputedExpressionWidget div.content {
position: relative;
}
div.ComputedExpressionWidget div.content input,
div.ComputedExpressionWidget div.content input:focus {
border-left-width: 3ex;
}
div.content.hint-error input[type],
.hint-error div.content input[type] {
border-color: red;
@ -1360,16 +1359,6 @@ div.ComputedExpressionWidget.hint-warning div.content span.hint-text {
display: block;
}
div.ComputedExpressionWidget div.content::before {
font-family: FontAwesome;
content: "\f1b2";
position: absolute;
top: 8px;
left: 6px;
box-sizing: border-box;
border: 1px solid transparent;
}
div.admin-permissions thead th {
transform: rotate(-45deg);
transform-origin: 10% 0;

View File

@ -452,7 +452,7 @@ class ExportToModel(WorkflowStatusItem):
if node.tag == DRAW_FRAME:
name = node.attrib.get(DRAW_NAME)
if not (name and name.startswith('=')):
if not self.get_expression(name)['type'] == 'python':
continue
# variable image
try:

View File

@ -113,7 +113,7 @@ class JumpWorkflowStatusItem(WorkflowStatusJumpItem):
self.timeout = None
else:
timeout = elem.text.encode(charset)
if timeout.startswith('='):
if self.get_expression(timeout)['type'] != 'text':
self.timeout = timeout
else:
self.timeout = int(timeout)
@ -183,7 +183,7 @@ class JumpWorkflowStatusItem(WorkflowStatusJumpItem):
'<br/><span class="warning">Minimal duration is %(granularity)s</span>')) % {
'variables': ', '.join(timewords()),
'granularity': seconds2humanduration(self._granularity)}
if str(self.timeout).startswith('='):
if not isinstance(self.timeout, int) and self.get_expression(self.timeout)['type'] != 'text':
form.add(ComputedExpressionWidget, '%stimeout' % prefix, title=_('Timeout'),
value=self.timeout, hint=_hint)
else:
@ -193,7 +193,7 @@ class JumpWorkflowStatusItem(WorkflowStatusJumpItem):
def timeout_parse(self, value):
if not value:
return value
if value.startswith('='):
if self.get_expression(value)['type'] != 'text':
return value
try:
return humanduration2seconds(value)

View File

@ -911,7 +911,7 @@ class XmlSerialisable(object):
# if a computed value is possible and value looks like
# an expression, use it
if value.startswith('=') or Template.is_template_string(value):
if WorkflowStatusItem.get_expression(value)['type'] in ('python', 'template'):
return value
# if the roles are managed by the idp, don't try further.
@ -1659,27 +1659,48 @@ class WorkflowStatusItem(XmlSerialisable):
value = getattr(self, '%s_parse' % f)(value)
setattr(self, f, value)
@classmethod
def get_expression(cls, var):
if not var:
expression_type = 'text'
expression_value = ''
elif var.startswith('='):
expression_type = 'python'
expression_value = var[1:]
elif '{{' in var or '{%' in var or '[' in var:
expression_type = 'template'
expression_value = var
else:
expression_type = 'text'
expression_value = var
return {'type': expression_type, 'value': expression_value}
@classmethod
def compute(cls, var, render=True, raises=False, context=None):
if not isinstance(var, basestring):
return var
if not var.startswith('=') and not render:
expression = cls.get_expression(var)
if expression['type'] != 'python' and not render:
return var
if expression['type'] == 'text':
return expression['value']
vars = get_publisher().substitutions.get_context_variables()
vars.update(context or {})
if not var.startswith('='):
if expression['type'] == 'template':
try:
return Template(var, raises=raises, autoescape=False).render(vars)
return Template(expression['value'], raises=raises, autoescape=False).render(vars)
except TemplateError:
if raises:
raise
return var
try:
return eval(var[1:], get_publisher().get_global_eval_dict(), vars)
return eval(expression['value'], get_publisher().get_global_eval_dict(), vars)
except:
if raises:
raise
@ -2002,6 +2023,12 @@ class ChoiceWorkflowStatusItem(WorkflowStatusJumpItem):
backoffice_info_text = None
require_confirmation = False
def get_label(self):
expression = self.get_expression(self.label)
if expression['type'] == 'text':
return expression['value']
return _('computed label')
def get_line_details(self):
if self.label:
more = ''
@ -2009,13 +2036,13 @@ class ChoiceWorkflowStatusItem(WorkflowStatusJumpItem):
more += ' ' + _('(and set marker)')
if self.by:
return _('"%(label)s" by %(by)s%(more)s') % {
'label' : self.label,
'label' : self.get_label(),
'by' : self.render_list_of_roles(self.by),
'more': more
}
else:
return _('"%(label)s"%(more)s') % {
'label': self.label,
'label': self.get_label(),
'more': more
}
else:
@ -2118,7 +2145,7 @@ class SendmailWorkflowStatusItem(WorkflowStatusItem):
return None
value = elem.text.encode(charset)
if value.startswith('=') or '@' in value or Template.is_template_string(value):
if self.get_expression(value)['type'] != 'text' or '@' in value:
return value
return super(SendmailWorkflowStatusItem, self)._get_role_id_from_xml(
@ -2133,10 +2160,11 @@ class SendmailWorkflowStatusItem(WorkflowStatusItem):
def render_list_of_roles_or_emails(self, roles):
t = []
for r in roles:
if r.startswith('=') or Template.is_template_string(r):
expression = self.get_expression(r)
if expression['type'] in ('python', 'template'):
t.append(_('computed value'))
elif '@' in r:
t.append(r)
elif '@' in expression['value']:
t.append(expression['value'])
else:
role_label = get_role_translation_label(self.parent.parent, r)
if role_label: