From 09f3aca5b960009c4afd136001f043eb66ac1a80 Mon Sep 17 00:00:00 2001 From: Timur Sufiev Date: Tue, 13 Aug 2013 20:33:39 +0400 Subject: [PATCH] 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 --- muranodashboard/panel/services/__init__.py | 10 +- .../panel/services/{__common.py => fields.py} | 302 ++---------------- muranodashboard/panel/services/forms.py | 258 +++++++++++++++ muranodashboard/panel/services/helpers.py | 46 +++ 4 files changed, 327 insertions(+), 289 deletions(-) rename muranodashboard/panel/services/{__common.py => fields.py} (52%) create mode 100644 muranodashboard/panel/services/forms.py create mode 100644 muranodashboard/panel/services/helpers.py diff --git a/muranodashboard/panel/services/__init__.py b/muranodashboard/panel/services/__init__.py index 0027ea0fb..9aaad622a 100644 --- a/muranodashboard/panel/services/__init__.py +++ b/muranodashboard/panel/services/__init__.py @@ -11,7 +11,6 @@ # 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 os from django.template.defaultfilters import slugify from collections import OrderedDict @@ -24,7 +23,7 @@ _all_services = OrderedDict() def import_all_services(): - from muranodashboard.panel.services.__common import decamelize + import muranodashboard.panel.services.helpers as utils directory = os.path.dirname(__file__) for fname in sorted(os.listdir(directory)): try: @@ -35,7 +34,7 @@ def import_all_services(): if (not name in _all_services or _all_services[name][0] < modified_on): with open(path) as f: - kwargs = {decamelize(k): v + kwargs = {utils.decamelize(k): v for k, v in yaml.load(f).iteritems()} _all_services[name] = (modified_on, type(name, (), kwargs)) @@ -46,14 +45,13 @@ def import_all_services(): def iterate_over_services(): + import muranodashboard.panel.services.forms as services import_all_services() for id, service_data in _all_services.items(): modified_on, service_cls = service_data - from muranodashboard.panel.services.__common import \ - ServiceConfigurationForm forms = [] for fields in service_cls.forms: - class Form(ServiceConfigurationForm): + class Form(services.ServiceConfigurationForm): service = service_cls fields_template = fields forms.append(Form) diff --git a/muranodashboard/panel/services/__common.py b/muranodashboard/panel/services/fields.py similarity index 52% rename from muranodashboard/panel/services/__common.py rename to muranodashboard/panel/services/fields.py index 384bd73c1..ae024a94a 100644 --- a/muranodashboard/panel/services/__common.py +++ b/muranodashboard/panel/services/fields.py @@ -25,11 +25,18 @@ from horizon import exceptions, messages from openstack_dashboard.api import glance from openstack_dashboard.api.nova import novaclient from muranodashboard.datagrids import DataGridCompound -import copy 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): @@ -41,6 +48,16 @@ class PasswordField(forms.CharField): password_re, _('The password must contain at least one letter, one \ 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 Media: js = ('muranodashboard/js/passwordfield.js',) @@ -98,16 +115,6 @@ class InstanceCountField(forms.IntegerField): 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): def __init__(self, *args, **kwargs): kwargs['widget'] = DataGridCompound @@ -214,64 +221,6 @@ class AZoneChoiceField(forms.ChoiceField): 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): def __init__(self, *args, **kwargs): kwargs['widget'] = forms.CheckboxInput(attrs={'class': 'checkbox'}) @@ -345,216 +294,3 @@ class DatabaseListField(forms.CharField): super(DatabaseListField, self).validate(value) for db_name in value: 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 diff --git a/muranodashboard/panel/services/forms.py b/muranodashboard/panel/services/forms.py new file mode 100644 index 000000000..8daf1b48c --- /dev/null +++ b/muranodashboard/panel/services/forms.py @@ -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 diff --git a/muranodashboard/panel/services/helpers.py b/muranodashboard/panel/services/helpers.py new file mode 100644 index 000000000..1292064cc --- /dev/null +++ b/muranodashboard/panel/services/helpers.py @@ -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