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:
parent
06012a4665
commit
a4bcaa1e60
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -14,7 +14,7 @@
|
||||
|
||||
import semantic_version
|
||||
|
||||
LATEST_FORMAT_VERSION = '2.3'
|
||||
LATEST_FORMAT_VERSION = '2.4'
|
||||
|
||||
|
||||
def check_version(version):
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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.
|
Loading…
x
Reference in New Issue
Block a user