Start refactoring dynamic UI.

1. When using $value for form field attribute, first try
cleaned_data. And only if it is absent, use raw data.

2. Move all service fields classes into separate module fields.py

3. Move some helpers to helpers.py and rename __common.py to forms.py.

Change-Id: Iba27cb2f9bc1c017443e0666aa274b5f88373045
This commit is contained in:
Timur Sufiev 2013-08-13 20:33:39 +04:00
parent 5998afd730
commit 09f3aca5b9
4 changed files with 327 additions and 289 deletions

View File

@ -11,7 +11,6 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import os import os
from django.template.defaultfilters import slugify from django.template.defaultfilters import slugify
from collections import OrderedDict from collections import OrderedDict
@ -24,7 +23,7 @@ _all_services = OrderedDict()
def import_all_services(): def import_all_services():
from muranodashboard.panel.services.__common import decamelize import muranodashboard.panel.services.helpers as utils
directory = os.path.dirname(__file__) directory = os.path.dirname(__file__)
for fname in sorted(os.listdir(directory)): for fname in sorted(os.listdir(directory)):
try: try:
@ -35,7 +34,7 @@ def import_all_services():
if (not name in _all_services or if (not name in _all_services or
_all_services[name][0] < modified_on): _all_services[name][0] < modified_on):
with open(path) as f: with open(path) as f:
kwargs = {decamelize(k): v kwargs = {utils.decamelize(k): v
for k, v in yaml.load(f).iteritems()} for k, v in yaml.load(f).iteritems()}
_all_services[name] = (modified_on, _all_services[name] = (modified_on,
type(name, (), kwargs)) type(name, (), kwargs))
@ -46,14 +45,13 @@ def import_all_services():
def iterate_over_services(): def iterate_over_services():
import muranodashboard.panel.services.forms as services
import_all_services() import_all_services()
for id, service_data in _all_services.items(): for id, service_data in _all_services.items():
modified_on, service_cls = service_data modified_on, service_cls = service_data
from muranodashboard.panel.services.__common import \
ServiceConfigurationForm
forms = [] forms = []
for fields in service_cls.forms: for fields in service_cls.forms:
class Form(ServiceConfigurationForm): class Form(services.ServiceConfigurationForm):
service = service_cls service = service_cls
fields_template = fields fields_template = fields
forms.append(Form) forms.append(Form)

View File

@ -25,11 +25,18 @@ from horizon import exceptions, messages
from openstack_dashboard.api import glance from openstack_dashboard.api import glance
from openstack_dashboard.api.nova import novaclient from openstack_dashboard.api.nova import novaclient
from muranodashboard.datagrids import DataGridCompound from muranodashboard.datagrids import DataGridCompound
import copy
from django.template.defaultfilters import pluralize from django.template.defaultfilters import pluralize
import copy
CONFIRM_ERR_DICT = {'required': _('Please confirm your password')} def with_request(func):
def update(self, initial):
request = initial.get('request')
if request:
func(self, request, initial)
else:
raise forms.ValidationError("Can't get a request information")
return update
class PasswordField(forms.CharField): class PasswordField(forms.CharField):
@ -41,6 +48,16 @@ class PasswordField(forms.CharField):
password_re, _('The password must contain at least one letter, one \ password_re, _('The password must contain at least one letter, one \
number and one special character'), 'invalid') number and one special character'), 'invalid')
@staticmethod
def get_clone_name(name):
return name + '-clone'
def compare(self, name, form_data):
if self.is_original(): # run compare only for original fields
if form_data.get(name) != form_data.get(self.get_clone_name(name)):
raise forms.ValidationError(_(u"{0}{1} don't match".format(
self.label, pluralize(2))))
class PasswordInput(forms.PasswordInput): class PasswordInput(forms.PasswordInput):
class Media: class Media:
js = ('muranodashboard/js/passwordfield.js',) js = ('muranodashboard/js/passwordfield.js',)
@ -98,16 +115,6 @@ class InstanceCountField(forms.IntegerField):
return value return value
def with_request(func):
def update(self, initial):
request = initial.get('request')
if request:
func(self, request, initial)
else:
raise forms.ValidationError("Can't get a request information")
return update
class DataGridField(forms.MultiValueField): class DataGridField(forms.MultiValueField):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
kwargs['widget'] = DataGridCompound kwargs['widget'] = DataGridCompound
@ -214,64 +221,6 @@ class AZoneChoiceField(forms.ChoiceField):
self.choices = az_choices self.choices = az_choices
def get_clone_name(name):
return name + '-clone'
def camelize(name):
return ''.join([bit.capitalize() for bit in name.split('_')])
def decamelize(name):
pat = re.compile(r'([A-Z]*[^A-Z]*)(.*)')
bits = []
while True:
head, tail = re.match(pat, name).groups()
bits.append(head)
if tail:
name = tail
else:
break
return '_'.join([bit.lower() for bit in bits])
def explode(string):
if not string:
return string
bits = []
while True:
head, tail = string[0], string[1:]
bits.append(head)
if tail:
string = tail
else:
break
return bits
class UpdatableFieldsForm(forms.Form):
def update_fields(self):
# duplicate all password fields
while True:
index, inserted = 0, False
for name, field in self.fields.iteritems():
if isinstance(field, PasswordField) and not field.has_clone:
self.fields.insert(index + 1,
get_clone_name(name),
field.clone_field())
inserted = True
break
index += 1
if not inserted:
break
for name, field in self.fields.iteritems():
if hasattr(field, 'update'):
field.update(self.initial)
if not field.required:
field.widget.attrs['placeholder'] = 'Optional'
class BooleanField(forms.BooleanField): class BooleanField(forms.BooleanField):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
kwargs['widget'] = forms.CheckboxInput(attrs={'class': 'checkbox'}) kwargs['widget'] = forms.CheckboxInput(attrs={'class': 'checkbox'})
@ -345,216 +294,3 @@ class DatabaseListField(forms.CharField):
super(DatabaseListField, self).validate(value) super(DatabaseListField, self).validate(value)
for db_name in value: for db_name in value:
self.validate_mssql_identifier(db_name) self.validate_mssql_identifier(db_name)
class ServiceConfigurationForm(UpdatableFieldsForm):
def __init__(self, *args, **kwargs):
super(ServiceConfigurationForm, self).__init__(*args, **kwargs)
self.attribute_mappings = {}
self.insert_fields(self.fields_template)
self.initial = kwargs.get('initial', self.initial)
self.update_fields()
EVAL_PREFIX = '$'
types = {
'string': forms.CharField,
'boolean': BooleanField,
'instance': InstanceCountField,
'clusterip': ClusterIPField,
'domain': DomainChoiceField,
'password': PasswordField,
'integer': forms.IntegerField,
'databaselist': DatabaseListField,
'datagrid': DataGridField,
'flavor': FlavorChoiceField,
'image': ImageChoiceField,
'azone': AZoneChoiceField,
'text': (forms.CharField, forms.Textarea)
}
localizable_keys = set(['label', 'help_text', 'error_messages'])
def init_attribute_mappings(self, field_name, kwargs):
def set_mapping(name, value):
"""Spawns new dictionaries for each dot found in name."""
bits = name.split('.')
head, tail, mapping = bits[0], bits[1:], self.attribute_mappings
while tail:
if not head in mapping:
mapping[head] = {}
head, tail, mapping = tail[0], tail[1:], mapping[head]
mapping[head] = value
if 'attribute_names' in kwargs:
attr_names = kwargs['attribute_names']
if type(attr_names) == list:
# allow pushing field value to multiple attributes
for attr_name in attr_names:
set_mapping(attr_name, field_name)
elif attr_names:
# if attributeNames = false, do not push field value
set_mapping(attr_names, field_name)
del kwargs['attribute_names']
else:
# default mapping: field to attr with same name
# do not spawn new dictionaries for any dot in field_name
self.attribute_mappings[field_name] = field_name
def init_field_descriptions(self, kwargs):
if 'description' in kwargs:
del kwargs['description']
if 'description_title' in kwargs:
del kwargs['description_title']
def insert_fields(self, field_specs):
def process_widget(kwargs, cls, widget):
widget = kwargs.get('widget', widget)
if widget is None:
widget = cls.widget
if 'widget_media' in kwargs:
media = kwargs['widget_media']
del kwargs['widget_media']
class Widget(widget):
class Media:
js = media.get('js', ())
css = media.get('css', {})
widget = Widget
if 'widget_attrs' in kwargs:
widget = widget(attrs=kwargs['widget_attrs'])
del kwargs['widget_attrs']
return widget
def append_properties(cls, kwargs):
props = {}
for key, value in kwargs.iteritems():
if isinstance(value, property):
props[key] = value
for key in props.keys():
del kwargs[key]
if props:
return type('cls_with_props', (cls,), props)
else:
return cls
def append_field(field_spec):
_, cls = parse_spec(field_spec['type'], 'type')
widget = None
if type(cls) == tuple:
cls, widget = cls
_, kwargs = parse_spec(field_spec)
kwargs['widget'] = process_widget(kwargs, cls, widget)
cls = append_properties(cls, kwargs)
self.init_attribute_mappings(field_spec['name'], kwargs)
self.init_field_descriptions(kwargs)
self.fields.insert(len(self.fields),
field_spec['name'],
cls(**kwargs))
def prepare_regexp(regexp):
if regexp[0] == '/':
groups = re.match(r'^/(.*)/([A-Za-z]*)$', regexp).groups()
regexp, flags_str = groups
flags = 0
for flag in explode(flags_str):
flag = flag.upper()
if hasattr(re, flag):
flags |= getattr(re, flag)
return RegexValidator(re.compile(regexp, flags))
else:
return RegexValidator(re.compile(regexp))
def is_localizable(keys):
return set(keys).intersection(self.localizable_keys)
def parse_spec(spec, keys=[]):
if not type(keys) == list:
keys = [keys]
key = keys and keys[-1] or None
if type(spec) == dict:
items = []
for k, v in spec.iteritems():
if not k in ('type', 'name'):
k = decamelize(k)
newKey, v = parse_spec(v, keys + [k])
if newKey:
k = newKey
items.append((k, v))
return key, dict(items)
elif type(spec) == list:
return key, [parse_spec(_spec, keys)[1] for _spec in spec]
elif type(spec) in (str, unicode) and is_localizable(keys):
return key, _(spec)
else:
if key == 'type':
return key, self.types[spec]
elif key == 'hidden' and spec is True:
return 'widget', forms.HiddenInput
elif key == 'regexp_validator':
return 'validators', [prepare_regexp(spec)]
elif (type(spec) in (str, unicode) and
spec[0] == self.EVAL_PREFIX):
def _get(field):
name = self.add_prefix(spec[1:])
return self.data.get(name, False)
def _set(field, value):
# doesn't work - why?
# super(field.__class__, field).__setattr__(key, value)
field.__dict__[key] = value
def _del(field):
# doesn't work - why?
# super(field.__class__, field).__delattr__(key)
del field.__dict__[key]
return key, property(_get, _set, _del)
else:
return key, spec
for spec in field_specs:
append_field(spec)
def get_unit_templates(self, data):
def parse_spec(spec):
if type(spec) == list:
return [parse_spec(_spec) for _spec in spec]
elif type(spec) == dict:
return {parse_spec(k): parse_spec(v)
for k, v in spec.iteritems()}
elif (type(spec) in (str, unicode) and
spec[0] == self.EVAL_PREFIX):
return data.get(spec[1:])
else:
return spec
return [parse_spec(spec) for spec in self.service.unit_templates]
def extract_attributes(self, attributes):
def get_data(name):
if type(name) == dict:
return {k: get_data(v) for k, v in name.iteritems()}
else:
return self.cleaned_data[name]
for attr_name, field_name in self.attribute_mappings.iteritems():
attributes[attr_name] = get_data(field_name)
def clean(self):
form_data = self.cleaned_data
def compare(name, label):
if form_data.get(name) != form_data.get(get_clone_name(name)):
raise forms.ValidationError(_(u"{0}{1} don't match".format(
label, pluralize(2))))
for name, field in self.fields.iteritems():
if isinstance(field, PasswordField) and field.is_original():
compare(name, field.label)
if hasattr(field, 'postclean'):
value = field.postclean(self, form_data)
if value:
self.cleaned_data[name] = value
return self.cleaned_data

View File

@ -0,0 +1,258 @@
# Copyright (c) 2013 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import re
from django import forms
from django.core.validators import RegexValidator
from django.utils.translation import ugettext_lazy as _
import muranodashboard.panel.services.fields as fields
import muranodashboard.panel.services.helpers as helpers
class UpdatableFieldsForm(forms.Form):
def update_fields(self):
# duplicate all password fields
while True:
index, inserted = 0, False
for name, field in self.fields.iteritems():
if isinstance(field, fields.PasswordField) and \
not field.has_clone:
self.fields.insert(index + 1,
field.get_clone_name(name),
field.clone_field())
inserted = True
break
index += 1
if not inserted:
break
for name, field in self.fields.iteritems():
if hasattr(field, 'update'):
field.update(self.initial)
if not field.required:
field.widget.attrs['placeholder'] = 'Optional'
class ServiceConfigurationForm(UpdatableFieldsForm):
def __init__(self, *args, **kwargs):
super(ServiceConfigurationForm, self).__init__(*args, **kwargs)
self.attribute_mappings = {}
self.insert_fields(self.fields_template)
self.initial = kwargs.get('initial', self.initial)
self.update_fields()
EVAL_PREFIX = '$'
types = {
'string': forms.CharField,
'boolean': fields.BooleanField,
'instance': fields.InstanceCountField,
'clusterip': fields.ClusterIPField,
'domain': fields.DomainChoiceField,
'password': fields.PasswordField,
'integer': forms.IntegerField,
'databaselist': fields.DatabaseListField,
'datagrid': fields.DataGridField,
'flavor': fields.FlavorChoiceField,
'image': fields.ImageChoiceField,
'azone': fields.AZoneChoiceField,
'text': (forms.CharField, forms.Textarea)
}
localizable_keys = set(['label', 'help_text', 'error_messages'])
def init_attribute_mappings(self, field_name, kwargs):
def set_mapping(name, value):
"""Spawns new dictionaries for each dot found in name."""
bits = name.split('.')
head, tail, mapping = bits[0], bits[1:], self.attribute_mappings
while tail:
if not head in mapping:
mapping[head] = {}
head, tail, mapping = tail[0], tail[1:], mapping[head]
mapping[head] = value
if 'attribute_names' in kwargs:
attr_names = kwargs['attribute_names']
if type(attr_names) == list:
# allow pushing field value to multiple attributes
for attr_name in attr_names:
set_mapping(attr_name, field_name)
elif attr_names:
# if attributeNames = false, do not push field value
set_mapping(attr_names, field_name)
del kwargs['attribute_names']
else:
# default mapping: field to attr with same name
# do not spawn new dictionaries for any dot in field_name
self.attribute_mappings[field_name] = field_name
def init_field_descriptions(self, kwargs):
if 'description' in kwargs:
del kwargs['description']
if 'description_title' in kwargs:
del kwargs['description_title']
def insert_fields(self, field_specs):
def process_widget(kwargs, cls, widget):
widget = kwargs.get('widget', widget)
if widget is None:
widget = cls.widget
if 'widget_media' in kwargs:
media = kwargs['widget_media']
del kwargs['widget_media']
class Widget(widget):
class Media:
js = media.get('js', ())
css = media.get('css', {})
widget = Widget
if 'widget_attrs' in kwargs:
widget = widget(attrs=kwargs['widget_attrs'])
del kwargs['widget_attrs']
return widget
def append_properties(cls, kwargs):
props = {}
for key, value in kwargs.iteritems():
if isinstance(value, property):
props[key] = value
for key in props.keys():
del kwargs[key]
if props:
return type('cls_with_props', (cls,), props)
else:
return cls
def append_field(field_spec):
cls = parse_spec(field_spec['type'], 'type')[1]
widget = None
if type(cls) == tuple:
cls, widget = cls
kwargs = parse_spec(field_spec)[1]
kwargs['widget'] = process_widget(kwargs, cls, widget)
cls = append_properties(cls, kwargs)
self.init_attribute_mappings(field_spec['name'], kwargs)
self.init_field_descriptions(kwargs)
self.fields.insert(len(self.fields),
field_spec['name'],
cls(**kwargs))
def prepare_regexp(regexp):
if regexp[0] == '/':
groups = re.match(r'^/(.*)/([A-Za-z]*)$', regexp).groups()
regexp, flags_str = groups
flags = 0
for flag in helpers.explode(flags_str):
flag = flag.upper()
if hasattr(re, flag):
flags |= getattr(re, flag)
return RegexValidator(re.compile(regexp, flags))
else:
return RegexValidator(re.compile(regexp))
def is_localizable(keys):
return set(keys).intersection(self.localizable_keys)
def parse_spec(spec, keys=[]):
if not type(keys) == list:
keys = [keys]
key = keys and keys[-1] or None
if type(spec) == dict:
items = []
for k, v in spec.iteritems():
if not k in ('type', 'name'):
k = helpers.decamelize(k)
newKey, v = parse_spec(v, keys + [k])
if newKey:
k = newKey
items.append((k, v))
return key, dict(items)
elif type(spec) == list:
return key, [parse_spec(_spec, keys)[1] for _spec in spec]
elif type(spec) in (str, unicode) and is_localizable(keys):
return key, _(spec)
else:
if key == 'type':
return key, self.types[spec]
elif key == 'hidden' and spec is True:
return 'widget', forms.HiddenInput
elif key == 'regexp_validator':
return 'validators', [prepare_regexp(spec)]
elif (type(spec) in (str, unicode) and
spec[0] == self.EVAL_PREFIX):
def _get(field):
"""First try to get value from cleaned data, if none
found, use raw data."""
data = getattr(self, 'cleaned_data', None)
value = data and data.get(spec[1:], None)
if value is None:
name = self.add_prefix(spec[1:])
value = self.data.get(name, None)
return value
def _set(field, value):
# doesn't work - why?
# super(field.__class__, field).__setattr__(key, value)
field.__dict__[key] = value
def _del(field):
# doesn't work - why?
# super(field.__class__, field).__delattr__(key)
del field.__dict__[key]
return key, property(_get, _set, _del)
else:
return key, spec
for spec in field_specs:
append_field(spec)
def get_unit_templates(self, data):
def parse_spec(spec):
if type(spec) == list:
return [parse_spec(_spec) for _spec in spec]
elif type(spec) == dict:
return {parse_spec(k): parse_spec(v)
for k, v in spec.iteritems()}
elif (type(spec) in (str, unicode) and
spec[0] == self.EVAL_PREFIX):
return data.get(spec[1:])
else:
return spec
return [parse_spec(spec) for spec in self.service.unit_templates]
def extract_attributes(self, attributes):
def get_data(name):
if type(name) == dict:
return {k: get_data(v) for k, v in name.iteritems()}
else:
return self.cleaned_data[name]
for attr_name, field_name in self.attribute_mappings.iteritems():
attributes[attr_name] = get_data(field_name)
def clean(self):
form_data = self.cleaned_data
for name, field in self.fields.iteritems():
if isinstance(field, fields.PasswordField):
field.compare(name, form_data)
if hasattr(field, 'postclean'):
value = field.postclean(self, form_data)
if value:
self.cleaned_data[name] = value
return self.cleaned_data

View File

@ -0,0 +1,46 @@
# Copyright (c) 2013 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import re
def camelize(name):
return ''.join([bit.capitalize() for bit in name.split('_')])
def decamelize(name):
pat = re.compile(r'([A-Z]*[^A-Z]*)(.*)')
bits = []
while True:
head, tail = re.match(pat, name).groups()
bits.append(head)
if tail:
name = tail
else:
break
return '_'.join([bit.lower() for bit in bits])
def explode(string):
if not string:
return string
bits = []
while True:
head, tail = string[0], string[1:]
bits.append(head)
if tail:
string = tail
else:
break
return bits