Add Parameters section to UI definition

- Parameters is a key/value dictionary section
where keys are YAQL variable names and values can be either
a YAQL expression or a prepopulated constants. Expressions are evaluated
at the very beginning of a form lifecycle.
- Parameters are accessible using regular YAQL variable syntax ($something)
- Any values within form definitions (title, initial, required etc.) now can be a YAQL expressions that are evaluated before for is rendered but after the parameters are ready so they can be used in the form definitions
- Latest UI definition version was raised to 2.4
- Also now it is possible to provide choices for the choice type control in a form of key/value (dictionary) in addition to list of tuples

Change-Id: I41605ec829d012a69327bc09277dddee5a922bef
This commit is contained in:
Stan Lagun 2016-10-19 23:54:10 -07:00
parent 06012a4665
commit a4bcaa1e60
7 changed files with 83 additions and 82 deletions

View File

@ -31,6 +31,7 @@ from horizon import messages
from openstack_dashboard.api import glance
from openstack_dashboard.api import nova
from oslo_log import log as logging
from oslo_log import versionutils
import six
from yaql import legacy
@ -38,8 +39,6 @@ from muranodashboard.api import packages as pkg_api
from muranodashboard.common import net
from muranodashboard.environments import api as env_api
from oslo_log import versionutils
LOG = logging.getLogger(__name__)
@ -135,17 +134,23 @@ class RawProperty(object):
def __init__(self, key, spec):
self.key = key
self.spec = spec
self.value = None
self.value_evaluated = False
def finalize(self, form_name, service):
def finalize(self, form_name, service, cls):
def _get(field):
data_ready, value = service.get_data(form_name, self.spec)
return value if data_ready else field.__dict__[self.key]
if self.value_evaluated:
return self.value
return service.get_data(form_name, self.spec)
def _set(field, value):
field.__dict__[self.key] = value
self.value = value
self.value_evaluated = value is not None
if hasattr(cls, self.key):
getattr(cls, self.key).fset(field, value)
def _del(field):
del field.__dict__[self.key]
_set(field, None)
return property(_get, _set, _del)
@ -197,7 +202,7 @@ class CustomPropertiesField(forms.Field):
kwargs_ = copy.copy(kwargs)
for key, value in kwargs_.items():
if isinstance(value, RawProperty):
props[key] = value.finalize(form_name, service)
props[key] = value.finalize(form_name, service, cls)
del kwargs[key]
if props:
return type(cls.__name__, (cls,), props)
@ -294,7 +299,13 @@ class IntegerField(forms.IntegerField, CustomPropertiesField):
class ChoiceField(forms.ChoiceField, CustomPropertiesField):
pass
def __init__(self, **kwargs):
choices = kwargs.get('choices') or getattr(self, 'choices', None)
if choices:
if isinstance(choices, dict):
choices = list(choices.items())
kwargs['choices'] = choices
super(ChoiceField, self).__init__(**kwargs)
class DynamicChoiceField(hz_forms.DynamicChoiceField, CustomPropertiesField):

View File

@ -13,7 +13,6 @@
# under the License.
from collections import defaultdict
import copy
from django import forms
from django.utils.translation import ugettext_lazy as _
@ -62,19 +61,6 @@ TYPES_KWARGS = {
def _collect_fields(field_specs, form_name, service):
def careful_deepcopy(x):
"""Careful handling of deepcopy object with recursive.
There is a recursive reference in YAQL expression
(since 1.0 version) and standard deepcopy can't handle this.
"""
original_validators = x.pop('validators', None)
result = copy.deepcopy(x)
if original_validators:
result['validators'] = original_validators
return result
def process_widget(cls, kwargs):
if isinstance(cls, tuple):
cls, _w = cls
@ -140,7 +126,7 @@ def _collect_fields(field_specs, form_name, service):
return name, cls(**kwargs)
return [make_field(careful_deepcopy(_spec)) for _spec in field_specs]
return [make_field(_spec) for _spec in field_specs]
class DynamicFormMetaclass(forms.forms.DeclarativeFieldsMetaclass):

View File

@ -40,9 +40,6 @@ LOG.info('Using cache directory located at {dir}'.format(
dir=consts.CACHE_DIR))
_apps = {}
class Service(object):
"""Murano Service representation object
@ -59,7 +56,7 @@ class Service(object):
stored at local file-system cache .
"""
def __init__(self, cleaned_data, version, fqn, forms=None, templates=None,
application=None, **kwargs):
application=None, parameters=None, **kwargs):
self.cleaned_data = cleaned_data
self.templates = templates or {}
self.spec_version = str(version)
@ -74,6 +71,14 @@ class Service(object):
self.context = legacy.create_context()
yaql_functions.register(self.context)
self.parameters = {}
for k, v in six.iteritems(parameters or {}):
if not k or not k[0].isalpha():
continue
v = helpers.evaluate(v, self.context)
self.parameters[k] = v
self.context[k] = v
self.forms = []
for key, value in six.iteritems(kwargs):
setattr(self, key, value)
@ -132,8 +137,7 @@ class Service(object):
if data:
self.update_cleaned_data(data, form_name=form_name)
data = self.cleaned_data
value = data and expr.evaluate(data=data, context=self.context)
return data != {}, value
return expr.evaluate(data=data, context=self.context)
def update_cleaned_data(self, data, form=None, form_name=None):
form_name = form_name or form.__class__.__name__
@ -159,16 +163,7 @@ def import_app(request, app_id):
version.check_version(app_version)
service = dict(
(helpers.decamelize(k), v) for (k, v) in six.iteritems(ui_desc))
global _apps # In-memory caching of dynamic UI forms
if app_id in _apps:
LOG.debug('Using in-memory forms for app {0}'.format(fqn))
app = _apps[app_id]
app.set_data(app_data)
else:
LOG.debug('Creating new forms for app {0}'.format(fqn))
app = _apps[app_id] = Service(app_data, app_version, fqn, **service)
return app
return Service(app_data, app_version, fqn, **service)
def condition_getter(request, kwargs):

View File

@ -14,7 +14,7 @@
import semantic_version
LATEST_FORMAT_VERSION = '2.3'
LATEST_FORMAT_VERSION = '2.4'
def check_version(version):

View File

@ -329,26 +329,39 @@ class TestFields(testtools.TestCase):
class TestRawProperty(testtools.TestCase):
def test_finalize(self):
class Control(object):
def __init__(self):
self.value = None
@property
def prop(self):
return self.value
@prop.setter
def prop(self, value):
self.value = value
@prop.deleter
def prop(self):
delattr(self, 'value')
mock_service = mock.Mock()
mock_service.get_data.side_effect = [
(True, 'foo_value'), (False, None)
]
mock_field = mock.MagicMock()
mock_field.__dict__['foo_key'] = 'foo_dict_value'
mock_service.get_data.side_effect = ['foo_value']
raw_property = fields.RawProperty('foo_key', 'foo_spec')
props = raw_property.finalize('foo_form_name', mock_service)
raw_property = fields.RawProperty('prop', 'foo_spec')
props = raw_property.finalize(
'foo_form_name', mock_service, Control)
result = props.fget(mock_field)
ctl = Control()
result = props.fget(ctl)
self.assertEqual('foo_value', result)
result = props.fget(mock_field)
self.assertEqual('foo_dict_value', result)
props.fset(mock_field, 'bar_value')
self.assertEqual('bar_value', mock_field.__dict__['foo_key'])
props.fset(ctl, 'bar_value')
self.assertEqual('bar_value', ctl.prop)
props.fdel(mock_field)
self.assertNotIn('foo_key', mock_field.__dict__)
props.fdel(ctl)
self.assertNotIn('prop', ctl.__dict__)
class TestCustomPropertiesField(testtools.TestCase):

View File

@ -144,20 +144,16 @@ class TestService(helpers.APITestCase):
fqn='io.murano.Test',
application=self.application)
service.set_data(cleaned_data)
have_data, result = service.get_data(catalog_forms.WF_MANAGEMENT_NAME,
expr)
result = service.get_data(catalog_forms.WF_MANAGEMENT_NAME, expr)
expected = {'workflowManagement': {'application_name': 'foobar'}}
self.assertTrue(have_data)
self.assertEqual(expected, result)
# Test whether passing data to get_data works.
service.set_data({})
cleaned_data = cleaned_data[catalog_forms.WF_MANAGEMENT_NAME]
have_data, result = service.get_data(catalog_forms.WF_MANAGEMENT_NAME,
expr,
data=cleaned_data)
self.assertTrue(have_data)
result = service.get_data(
catalog_forms.WF_MANAGEMENT_NAME, expr, data=cleaned_data)
self.assertEqual(expected, result)
def test_get_apps_data(self):
@ -165,36 +161,18 @@ class TestService(helpers.APITestCase):
self.assertEqual({}, result)
self.assertEqual(self.request.session['apps_data'], result)
@mock.patch.object(services, 'LOG')
@mock.patch.object(services, 'pkg_api')
def test_import_app(self, mock_pkg_api, mock_log):
def test_import_app(self, mock_pkg_api):
mock_pkg_api.get_app_ui.return_value = {
'foo': 'bar',
'application': self.application
}
mock_pkg_api.get_app_fqn.return_value = 'foo_fqn'
services._apps = {}
service = services.import_app(self.request, '123')
self.assertEqual(services.Service, type(service))
self.assertEqual('bar', service.foo)
self.assertEqual(self.application, service.application)
self.assertIn('123', services._apps)
self.assertEqual(services._apps['123'], service)
mock_log.debug.assert_any_call(
'Using data {0} for app {1}'.format({}, 'foo_fqn'))
mock_log.debug.assert_any_call(
'Creating new forms for app {0}'.format('foo_fqn'))
# Test whether the service was cached.
service = services.import_app(self.request, '123')
self.assertEqual(services.Service, type(service))
self.assertEqual('bar', service.foo)
self.assertEqual(self.application, service.application)
self.assertIn('123', services._apps)
self.assertEqual(services._apps['123'], service)
mock_log.debug.assert_any_call(
'Using in-memory forms for app {0}'.format('foo_fqn'))
@mock.patch.object(services, 'pkg_api')
def test_condition_getter_with_stay_at_the_catalog(self, mock_pkg_api):
@ -286,7 +264,7 @@ class TestService(helpers.APITestCase):
@mock.patch.object(services, 'pkg_api')
def test_get_app_forms(self, mock_pkg_api):
mock_pkg_api.get_app_ui.return_value = {}
mock_pkg_api.get_app_ui.return_value = {'Application': {}}
kwargs = {'app_id': '123'}
result = services.get_app_forms(self.request, kwargs)

View File

@ -0,0 +1,18 @@
---
features:
- >
New section ``Parameters`` was added to UI definition markup. Parameters
is a key-value storage, whose values are available as YAQL variables. Thus
if the section has a key ``var`` its value can be retrieved using ``$var``
syntax and used anywhere in the markup - both as a field attribute values
and in Application/Templates sections. Parameter values can be a YAQL
expressions. The difference between Templates and Parameters is that
Parameters are evaluated once before form render whereas Templates are
evaluated on each access.
- >
``choice`` field type now can accept list of choices in a form of
dictionary. I.e. in addition to [[key1, value1], [key2, value2]] one can
provide {key1: value1, key2: value2}
- >
UI definition version was bumped to ``2.4``. If application is going to
use Parameters it should indicate it by setting the version in UI file.