Use FormsetStep for selecting flavors in the resource class workflow.
Instead of using data tables, use a django formset for displaying the list of available flavor templates. There is a new step, FormsetStep added for the purpose. Additional code for handling validation is added to the step's action, and code for rendering the formset in a way similar to the data table is added to the step's template. Currently the code only allows selecting the templates and setting their max_vms, but it is possible to modify this solution to allow editing the class templates in place and even adding new ones directly from the resource class form. Change-Id: If57d01822a85abb1c496bb705fc28644aecbe94a Closes-Bug: #1220240
This commit is contained in:
		@@ -36,9 +36,9 @@ class NodesFilterAction(tables.FilterAction):
 | 
			
		||||
 | 
			
		||||
class NodesTable(tables.DataTable):
 | 
			
		||||
    service_host = tables.Column("service_host",
 | 
			
		||||
                                 link=("horizon:infrastructure:"
 | 
			
		||||
                                       "resource_management:nodes:detail"),
 | 
			
		||||
                                 verbose_name=_("Name"))
 | 
			
		||||
                        link=("horizon:infrastructure:"
 | 
			
		||||
                            "resource_management:nodes:detail"),
 | 
			
		||||
                        verbose_name=_("Name"))
 | 
			
		||||
    mac_address = tables.Column("mac_address", verbose_name=_("MAC Address"))
 | 
			
		||||
    pm_address = tables.Column("pm_address", verbose_name=_("IP Address"))
 | 
			
		||||
    status = tables.Column("status",
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@
 | 
			
		||||
#    under the License.
 | 
			
		||||
 | 
			
		||||
from django.core import urlresolvers
 | 
			
		||||
from django.forms import formsets
 | 
			
		||||
from django.utils.translation import ugettext_lazy as _  # noqa
 | 
			
		||||
 | 
			
		||||
from horizon import exceptions
 | 
			
		||||
@@ -20,6 +21,9 @@ from horizon import forms
 | 
			
		||||
from horizon import messages
 | 
			
		||||
 | 
			
		||||
from tuskar_ui import api as tuskar
 | 
			
		||||
from tuskar_ui import forms as tuskar_forms
 | 
			
		||||
from tuskar_ui.infrastructure.resource_management.flavor_templates import\
 | 
			
		||||
    forms as flavor_templates_forms
 | 
			
		||||
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
@@ -64,3 +68,44 @@ class DeleteCommand(object):
 | 
			
		||||
            redirect = urlresolvers.reverse(
 | 
			
		||||
                            'horizon:infrastructure:resource_management:index')
 | 
			
		||||
            exceptions.handle(self.request, self.msg, redirect=redirect)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Inherit from the EditFlavorTemplate form to get all the nice validation.
 | 
			
		||||
class FlavorTemplatesForm(flavor_templates_forms.EditFlavorTemplate):
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
 | 
			
		||||
        # Drop the ``request`` parameter, as Formsets don't support it
 | 
			
		||||
        request = None
 | 
			
		||||
        super(FlavorTemplatesForm, self).__init__(request, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
        # Add a field for selecting the flavors
 | 
			
		||||
        selected_field = forms.BooleanField(label='', required=False)
 | 
			
		||||
        self.fields.insert(0, 'selected', selected_field)
 | 
			
		||||
 | 
			
		||||
        # Add nice widget types and classes
 | 
			
		||||
        self.fields['name'].widget = forms.TextInput(
 | 
			
		||||
            attrs={'class': 'input input-small'},
 | 
			
		||||
        )
 | 
			
		||||
        for name in ['cpu', 'memory', 'storage', 'ephemeral_disk',
 | 
			
		||||
                     'swap_disk']:
 | 
			
		||||
            self.fields[name].widget = tuskar_forms.NumberInput(
 | 
			
		||||
                    attrs={'class': 'number_input_slim'},
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    max_vms = forms.IntegerField(
 | 
			
		||||
        label=_("Max. VMs"),
 | 
			
		||||
        min_value=0,
 | 
			
		||||
        initial=0,
 | 
			
		||||
        widget=tuskar_forms.NumberInput(
 | 
			
		||||
            attrs={'class': 'number_input_slim'},
 | 
			
		||||
        ),
 | 
			
		||||
        required=False,
 | 
			
		||||
    )
 | 
			
		||||
    # Long name as used in the validation of EditFlavorTemplate.
 | 
			
		||||
    flavor_template_id = forms.IntegerField(widget=forms.HiddenInput())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FlavorTemplatesFormset = formsets.formset_factory(
 | 
			
		||||
    FlavorTemplatesForm,
 | 
			
		||||
    extra=0,
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -73,9 +73,14 @@ class ResourceClassViewTests(test.BaseAdminViewTests):
 | 
			
		||||
        url = urlresolvers.reverse(
 | 
			
		||||
            'horizon:infrastructure:resource_management:resource_classes:'
 | 
			
		||||
                'create')
 | 
			
		||||
        form_data = {'name': new_unique_name,
 | 
			
		||||
                     'service_type': new_resource_class.service_type,
 | 
			
		||||
                     'image': 'compute-img'}
 | 
			
		||||
        form_data = {
 | 
			
		||||
            'name': new_unique_name,
 | 
			
		||||
            'service_type': new_resource_class.service_type,
 | 
			
		||||
            'image': 'compute-img',
 | 
			
		||||
            'form-TOTAL_FORMS': 0,
 | 
			
		||||
            'form-INITIAL_FORMS': 0,
 | 
			
		||||
            'form-MAX_NUM_FORMS': 1000,
 | 
			
		||||
        }
 | 
			
		||||
        res = self.client.post(url, form_data)
 | 
			
		||||
        self.assertNoFormErrors(res)
 | 
			
		||||
        self.assertMessageCount(success=1)
 | 
			
		||||
@@ -107,9 +112,14 @@ class ResourceClassViewTests(test.BaseAdminViewTests):
 | 
			
		||||
        url = urlresolvers.reverse(
 | 
			
		||||
                'horizon:infrastructure:resource_management:'
 | 
			
		||||
                'resource_classes:create')
 | 
			
		||||
        form_data = {'name': new_unique_name,
 | 
			
		||||
                     'service_type': new_resource_class.service_type,
 | 
			
		||||
                     'image': 'compute-img'}
 | 
			
		||||
        form_data = {
 | 
			
		||||
            'name': new_unique_name,
 | 
			
		||||
            'service_type': new_resource_class.service_type,
 | 
			
		||||
            'image': 'compute-img',
 | 
			
		||||
            'form-TOTAL_FORMS': 0,
 | 
			
		||||
            'form-INITIAL_FORMS': 0,
 | 
			
		||||
            'form-MAX_NUM_FORMS': 1000,
 | 
			
		||||
        }
 | 
			
		||||
        res = self.client.post(url, form_data)
 | 
			
		||||
        self.assertRedirectsNoFollow(res,
 | 
			
		||||
            ("%s?tab=resource_management_tabs__resource_classes_tab" %
 | 
			
		||||
@@ -176,7 +186,8 @@ class ResourceClassViewTests(test.BaseAdminViewTests):
 | 
			
		||||
                'horizon:infrastructure:resource_management:index'))
 | 
			
		||||
 | 
			
		||||
    @test.create_stubs({
 | 
			
		||||
        tuskar.ResourceClass: ('get', 'list', 'update', 'set_racks')
 | 
			
		||||
        tuskar.ResourceClass: ('get', 'list', 'update', 'set_racks',
 | 
			
		||||
                               'all_flavors', 'flavortemplates_ids')
 | 
			
		||||
    })
 | 
			
		||||
    def test_edit_resource_class_post(self):
 | 
			
		||||
        resource_class = self.tuskar_resource_classes.first()
 | 
			
		||||
@@ -186,6 +197,11 @@ class ResourceClassViewTests(test.BaseAdminViewTests):
 | 
			
		||||
        tuskar.ResourceClass.get(
 | 
			
		||||
            mox.IsA(http.HttpRequest), resource_class.id).AndReturn(
 | 
			
		||||
                resource_class)
 | 
			
		||||
        tuskar.ResourceClass.get(
 | 
			
		||||
            mox.IsA(http.HttpRequest), resource_class.id).AndReturn(
 | 
			
		||||
                resource_class)
 | 
			
		||||
        tuskar.ResourceClass.all_flavors = []
 | 
			
		||||
        tuskar.ResourceClass.flavortemplates_ids = []
 | 
			
		||||
        tuskar.ResourceClass.list(
 | 
			
		||||
            mox.IsA(http.request.HttpRequest)).AndReturn(
 | 
			
		||||
                self.tuskar_resource_classes.list())
 | 
			
		||||
@@ -198,10 +214,15 @@ class ResourceClassViewTests(test.BaseAdminViewTests):
 | 
			
		||||
                                       add_racks_ids)
 | 
			
		||||
        self.mox.ReplayAll()
 | 
			
		||||
 | 
			
		||||
        form_data = {'resource_class_id': resource_class.id,
 | 
			
		||||
                     'name': resource_class.name,
 | 
			
		||||
                     'service_type': resource_class.service_type,
 | 
			
		||||
                     'image': 'compute-img'}
 | 
			
		||||
        form_data = {
 | 
			
		||||
            'resource_class_id': resource_class.id,
 | 
			
		||||
            'name': resource_class.name,
 | 
			
		||||
            'service_type': resource_class.service_type,
 | 
			
		||||
            'image': 'compute-img',
 | 
			
		||||
            'form-TOTAL_FORMS': 0,
 | 
			
		||||
            'form-INITIAL_FORMS': 0,
 | 
			
		||||
            'form-MAX_NUM_FORMS': 1000,
 | 
			
		||||
        }
 | 
			
		||||
        url = urlresolvers.reverse(
 | 
			
		||||
            'horizon:infrastructure:resource_management:resource_classes:'
 | 
			
		||||
                'update',
 | 
			
		||||
@@ -417,7 +438,8 @@ class ResourceClassViewTests(test.BaseAdminViewTests):
 | 
			
		||||
        self.assertEqual(res.status_code, 200)
 | 
			
		||||
 | 
			
		||||
    @test.create_stubs({
 | 
			
		||||
        tuskar.ResourceClass: ('get', 'list', 'update', 'set_racks')
 | 
			
		||||
        tuskar.ResourceClass: ('get', 'list', 'update', 'set_racks',
 | 
			
		||||
                               'all_flavors', 'flavortemplates_ids')
 | 
			
		||||
    })
 | 
			
		||||
    def test_detail_edit_racks_post(self):
 | 
			
		||||
        resource_class = self.tuskar_resource_classes.first()
 | 
			
		||||
@@ -427,6 +449,11 @@ class ResourceClassViewTests(test.BaseAdminViewTests):
 | 
			
		||||
        tuskar.ResourceClass.get(
 | 
			
		||||
            mox.IsA(http.HttpRequest), resource_class.id).AndReturn(
 | 
			
		||||
                resource_class)
 | 
			
		||||
        tuskar.ResourceClass.get(
 | 
			
		||||
            mox.IsA(http.HttpRequest), resource_class.id).AndReturn(
 | 
			
		||||
                resource_class)
 | 
			
		||||
        tuskar.ResourceClass.all_flavors = []
 | 
			
		||||
        tuskar.ResourceClass.flavortemplates_ids = []
 | 
			
		||||
        tuskar.ResourceClass.list(
 | 
			
		||||
            mox.IsA(http.request.HttpRequest)).AndReturn(
 | 
			
		||||
                self.tuskar_resource_classes.list())
 | 
			
		||||
@@ -439,10 +466,15 @@ class ResourceClassViewTests(test.BaseAdminViewTests):
 | 
			
		||||
                                       add_racks_ids)
 | 
			
		||||
        self.mox.ReplayAll()
 | 
			
		||||
 | 
			
		||||
        form_data = {'resource_class_id': resource_class.id,
 | 
			
		||||
                     'name': resource_class.name,
 | 
			
		||||
                     'service_type': resource_class.service_type,
 | 
			
		||||
                     'image': 'compute-img'}
 | 
			
		||||
        form_data = {
 | 
			
		||||
            'resource_class_id': resource_class.id,
 | 
			
		||||
            'name': resource_class.name,
 | 
			
		||||
            'service_type': resource_class.service_type,
 | 
			
		||||
            'image': 'compute-img',
 | 
			
		||||
            'form-TOTAL_FORMS': 0,
 | 
			
		||||
            'form-INITIAL_FORMS': 0,
 | 
			
		||||
            'form-MAX_NUM_FORMS': 1000,
 | 
			
		||||
        }
 | 
			
		||||
        url = urlresolvers.reverse(
 | 
			
		||||
            'horizon:infrastructure:resource_management:resource_classes:'
 | 
			
		||||
                'update_racks',
 | 
			
		||||
@@ -497,7 +529,8 @@ class ResourceClassViewTests(test.BaseAdminViewTests):
 | 
			
		||||
        self.assertEqual(res.status_code, 200)
 | 
			
		||||
 | 
			
		||||
    @test.create_stubs({
 | 
			
		||||
        tuskar.ResourceClass: ('get', 'list', 'update', 'set_racks')
 | 
			
		||||
        tuskar.ResourceClass: ('get', 'list', 'update', 'set_racks',
 | 
			
		||||
                               'all_flavors', 'flavortemplates_ids')
 | 
			
		||||
    })
 | 
			
		||||
    def test_detail_edit_flavors_post(self):
 | 
			
		||||
        resource_class = self.tuskar_resource_classes.first()
 | 
			
		||||
@@ -507,6 +540,11 @@ class ResourceClassViewTests(test.BaseAdminViewTests):
 | 
			
		||||
        tuskar.ResourceClass.get(
 | 
			
		||||
            mox.IsA(http.HttpRequest), resource_class.id).AndReturn(
 | 
			
		||||
                resource_class)
 | 
			
		||||
        tuskar.ResourceClass.get(
 | 
			
		||||
            mox.IsA(http.HttpRequest), resource_class.id).AndReturn(
 | 
			
		||||
                resource_class)
 | 
			
		||||
        tuskar.ResourceClass.all_flavors = []
 | 
			
		||||
        tuskar.ResourceClass.flavortemplates_ids = []
 | 
			
		||||
        tuskar.ResourceClass.list(
 | 
			
		||||
            mox.IsA(http.request.HttpRequest)).AndReturn(
 | 
			
		||||
                self.tuskar_resource_classes.list())
 | 
			
		||||
@@ -519,10 +557,15 @@ class ResourceClassViewTests(test.BaseAdminViewTests):
 | 
			
		||||
                                       add_racks_ids)
 | 
			
		||||
        self.mox.ReplayAll()
 | 
			
		||||
 | 
			
		||||
        form_data = {'resource_class_id': resource_class.id,
 | 
			
		||||
                     'name': resource_class.name,
 | 
			
		||||
                     'service_type': resource_class.service_type,
 | 
			
		||||
                     'image': 'compute-img'}
 | 
			
		||||
        form_data = {
 | 
			
		||||
            'resource_class_id': resource_class.id,
 | 
			
		||||
            'name': resource_class.name,
 | 
			
		||||
            'service_type': resource_class.service_type,
 | 
			
		||||
            'image': 'compute-img',
 | 
			
		||||
            'form-TOTAL_FORMS': 0,
 | 
			
		||||
            'form-INITIAL_FORMS': 0,
 | 
			
		||||
            'form-MAX_NUM_FORMS': 1000,
 | 
			
		||||
        }
 | 
			
		||||
        url = urlresolvers.reverse(
 | 
			
		||||
            'horizon:infrastructure:resource_management:resource_classes:'
 | 
			
		||||
                'update_flavors',
 | 
			
		||||
 
 | 
			
		||||
@@ -23,8 +23,8 @@ from horizon import workflows
 | 
			
		||||
from tuskar_ui import api as tuskar
 | 
			
		||||
import tuskar_ui.workflows
 | 
			
		||||
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
from tuskar_ui.infrastructure.resource_management.resource_classes\
 | 
			
		||||
    import forms as resource_classes_forms
 | 
			
		||||
from tuskar_ui.infrastructure.resource_management.resource_classes\
 | 
			
		||||
    import tables
 | 
			
		||||
 | 
			
		||||
@@ -46,11 +46,9 @@ class ResourceClassInfoAndFlavorsAction(workflows.Action):
 | 
			
		||||
                                         attrs={'class': 'switchable'})
 | 
			
		||||
                                     )
 | 
			
		||||
    image = forms.ChoiceField(label=_('Provisioning Image'),
 | 
			
		||||
                              required=True,
 | 
			
		||||
                              choices=[('compute-img', ('overcloud-compute'))],
 | 
			
		||||
                              widget=forms.Select(
 | 
			
		||||
                                  attrs={'class': 'switchable'})
 | 
			
		||||
                              )
 | 
			
		||||
                            required=True,
 | 
			
		||||
                            choices=[('compute-img', ('overcloud-compute'))],
 | 
			
		||||
                            widget=forms.Select(attrs={'class': 'switchable'}))
 | 
			
		||||
 | 
			
		||||
    def clean(self):
 | 
			
		||||
        cleaned_data = super(ResourceClassInfoAndFlavorsAction,
 | 
			
		||||
@@ -73,7 +71,18 @@ class ResourceClassInfoAndFlavorsAction(workflows.Action):
 | 
			
		||||
                      ' another resource class.')
 | 
			
		||||
                    % name
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        formset = self.initial.get('_formsets', {}).get('flavors')
 | 
			
		||||
        if formset:
 | 
			
		||||
            if formset.is_valid():
 | 
			
		||||
                cleaned_data['flavors'] = [
 | 
			
		||||
                    form.cleaned_data
 | 
			
		||||
                    for form in formset
 | 
			
		||||
                    if form.cleaned_data.get('selected')
 | 
			
		||||
                ]
 | 
			
		||||
            else:
 | 
			
		||||
                raise forms.ValidationError(
 | 
			
		||||
                    _('Errors in the flavors list.'),
 | 
			
		||||
                )
 | 
			
		||||
        return cleaned_data
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
@@ -82,47 +91,26 @@ class ResourceClassInfoAndFlavorsAction(workflows.Action):
 | 
			
		||||
                      "settings and add flavors to class.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CreateResourceClassInfoAndFlavors(tuskar_ui.workflows.TableStep):
 | 
			
		||||
    table_classes = (tables.FlavorTemplatesTable,)
 | 
			
		||||
class CreateResourceClassInfoAndFlavors(tuskar_ui.workflows.FormsetStep):
 | 
			
		||||
    formset_definitions = (
 | 
			
		||||
        ('flavors', resource_classes_forms.FlavorTemplatesFormset),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    action_class = ResourceClassInfoAndFlavorsAction
 | 
			
		||||
    template_name = 'infrastructure/resource_management/resource_classes/'\
 | 
			
		||||
                    '_resource_class_info_and_flavors_step.html'
 | 
			
		||||
    contributes = ("name", "service_type", "flavors_object_ids",
 | 
			
		||||
                   'max_vms')
 | 
			
		||||
 | 
			
		||||
    def contribute(self, data, context):
 | 
			
		||||
        request = self.workflow.request
 | 
			
		||||
        if data:
 | 
			
		||||
            context["flavors_object_ids"] =\
 | 
			
		||||
                request.POST.getlist("flavors_object_ids")
 | 
			
		||||
 | 
			
		||||
            # todo: lsmola django can't parse dictionaruy from POST
 | 
			
		||||
            # this should be rewritten to django formset
 | 
			
		||||
            context["max_vms"] = {}
 | 
			
		||||
            for index, value in request.POST.items():
 | 
			
		||||
                match = re.match(
 | 
			
		||||
                    '^(flavors_object_ids__max_vms__(.*?))$',
 | 
			
		||||
                    index)
 | 
			
		||||
                if match:
 | 
			
		||||
                    context["max_vms"][match.groups()[1]] = value
 | 
			
		||||
 | 
			
		||||
        context.update(data)
 | 
			
		||||
        return context
 | 
			
		||||
    contributes = ("name", "service_type", "flavors")
 | 
			
		||||
 | 
			
		||||
    def get_flavors_data(self):
 | 
			
		||||
        flavortemplates_ids = []
 | 
			
		||||
        try:
 | 
			
		||||
            resource_class_id = self.workflow.context.get("resource_class_id")
 | 
			
		||||
            if resource_class_id:
 | 
			
		||||
                resource_class = tuskar.ResourceClass.get(
 | 
			
		||||
                    self.workflow.request,
 | 
			
		||||
                    resource_class_id)
 | 
			
		||||
 | 
			
		||||
                # TODO(lsmola ugly interface, rewrite)
 | 
			
		||||
                self._tables['flavors'].active_multi_select_values = \
 | 
			
		||||
                    resource_class.flavortemplates_ids
 | 
			
		||||
 | 
			
		||||
                all_flavors = resource_class.all_flavors
 | 
			
		||||
                flavortemplates_ids = resource_class.flavortemplates_ids
 | 
			
		||||
            else:
 | 
			
		||||
                all_flavors = tuskar.FlavorTemplate.list(
 | 
			
		||||
                        self.workflow.request)
 | 
			
		||||
@@ -130,7 +118,21 @@ class CreateResourceClassInfoAndFlavors(tuskar_ui.workflows.TableStep):
 | 
			
		||||
            all_flavors = []
 | 
			
		||||
            exceptions.handle(self.workflow.request,
 | 
			
		||||
                              _('Unable to retrieve resource flavors list.'))
 | 
			
		||||
        return all_flavors
 | 
			
		||||
        flavors_data = []
 | 
			
		||||
        for flavor_template in all_flavors:
 | 
			
		||||
            data = {
 | 
			
		||||
                'selected': flavor_template.id in flavortemplates_ids,
 | 
			
		||||
                'name': flavor_template.name,
 | 
			
		||||
                'flavor_template_id': flavor_template.id,
 | 
			
		||||
                'max_vms': getattr(flavor_template, 'max_vms', None),
 | 
			
		||||
            }
 | 
			
		||||
            for capacity_name in ['cpu', 'memory', 'storage', 'ephemeral_disk',
 | 
			
		||||
                                  'swap_disk']:
 | 
			
		||||
                capacity = getattr(flavor_template, capacity_name, None)
 | 
			
		||||
                capacity_value = getattr(capacity, 'value', '')
 | 
			
		||||
                data[capacity_name] = capacity_value
 | 
			
		||||
            flavors_data.append(data)
 | 
			
		||||
        return flavors_data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RacksAction(workflows.Action):
 | 
			
		||||
@@ -202,11 +204,13 @@ class ResourceClassWorkflowMixin:
 | 
			
		||||
 | 
			
		||||
    def _get_flavors(self, request, data):
 | 
			
		||||
        flavors = []
 | 
			
		||||
        flavor_ids = data.get('flavors_object_ids') or []
 | 
			
		||||
        max_vms = data.get('max_vms')
 | 
			
		||||
        resource_class_name = data['name']
 | 
			
		||||
        for template_id in flavor_ids:
 | 
			
		||||
            template = tuskar.FlavorTemplate.get(request, template_id)
 | 
			
		||||
        for flavor in data.get('flavors') or []:
 | 
			
		||||
            flavor_id = flavor.get('flavor_template_id')
 | 
			
		||||
            if not flavor_id:
 | 
			
		||||
                continue
 | 
			
		||||
            # FIXME: thesheep maybe use the values from the formset here
 | 
			
		||||
            template = tuskar.FlavorTemplate.get(request, flavor_id)
 | 
			
		||||
            capacities = []
 | 
			
		||||
            for c in template.capacities:
 | 
			
		||||
                capacities.append({'name': c.name,
 | 
			
		||||
@@ -216,7 +220,7 @@ class ResourceClassWorkflowMixin:
 | 
			
		||||
            # e.g. m1.large, we add rc name to the template name:
 | 
			
		||||
            flavor_name = "%s.%s" % (resource_class_name, template.name)
 | 
			
		||||
            flavors.append({'name': flavor_name,
 | 
			
		||||
                            'max_vms': max_vms.get(template.id, None),
 | 
			
		||||
                            'max_vms': flavor.get('max_vms'),
 | 
			
		||||
                            'capacities': capacities})
 | 
			
		||||
        return flavors
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
<noscript><h3>{{ step }}</h3></noscript>
 | 
			
		||||
<noscript><h3>{{ step }}</h3>xx</noscript>
 | 
			
		||||
<table class="table-fixed">
 | 
			
		||||
  <tbody>
 | 
			
		||||
    <tr>
 | 
			
		||||
@@ -13,10 +13,50 @@
 | 
			
		||||
</table>
 | 
			
		||||
 | 
			
		||||
<div id="id_resource_class_flavors_table">
 | 
			
		||||
  {{ flavors_table.render }}
 | 
			
		||||
    {{ flavors_formset.management_form }}
 | 
			
		||||
    {% if flavors_formset.non_field_errors %}
 | 
			
		||||
        <div class="alert alert-error">
 | 
			
		||||
            {{ flavors_formset.non_field_errors }}
 | 
			
		||||
        </div>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
    <table class="table table-bordered">
 | 
			
		||||
        <thead>
 | 
			
		||||
            <tr class="table_caption"></tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
                {% for field in flavors_formset.0.visible_fields %}
 | 
			
		||||
                    <th class="normal_column">{{ field.field.label }}</th>
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </tr></thead>
 | 
			
		||||
        <tbody>
 | 
			
		||||
        {% for form in flavors_formset %}
 | 
			
		||||
            <tr>
 | 
			
		||||
                {% for field in form.visible_fields %}
 | 
			
		||||
                    <td class="control-group{% if field.errors %} error{% endif %}">
 | 
			
		||||
                        {{ field }}
 | 
			
		||||
                        {% for error in field.errors %}
 | 
			
		||||
                        <span class="help-inline">{{ error }}</span>
 | 
			
		||||
                        {% endfor %}
 | 
			
		||||
                        {%  if forloop.first %}
 | 
			
		||||
                            {% for field in form.hidden_fields %}
 | 
			
		||||
                                {{ field }}
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                            {% if form.non_field_errors %}
 | 
			
		||||
                                <div class="alert alert-error">
 | 
			
		||||
                                    {{ form.non_field_errors }}
 | 
			
		||||
                                </div>
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    </td>
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </tr>
 | 
			
		||||
        {% endfor %}
 | 
			
		||||
        </tbody>
 | 
			
		||||
        <tfoot>
 | 
			
		||||
            <tr><td colspan="{{ flavors_formset.0.visible_fields|length }}"></tr>
 | 
			
		||||
        </tfoot>
 | 
			
		||||
    </table>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
<script type="text/javascript">
 | 
			
		||||
    // show the flavors table only when service_type is compute
 | 
			
		||||
    var toggle_table = function(value){
 | 
			
		||||
@@ -51,6 +91,4 @@
 | 
			
		||||
    $(".modal #flavors input[type=checkbox]").bind('click', function() {
 | 
			
		||||
       toggle_max_vms($(this));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -25,6 +25,100 @@ import horizon.workflows
 | 
			
		||||
LOG = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FormsetStep(horizon.workflows.Step):
 | 
			
		||||
    """
 | 
			
		||||
    A workflow step that can render Django FormSets.
 | 
			
		||||
 | 
			
		||||
    This distinct class is required due to the complexity involved in handling
 | 
			
		||||
    both dynamic tab loading, dynamic table updating and table actions all
 | 
			
		||||
    within one view.
 | 
			
		||||
 | 
			
		||||
    .. attribute:: formset_definitions
 | 
			
		||||
 | 
			
		||||
        An iterable of tuples of {{ formset_name }} and formset class
 | 
			
		||||
        which this tab will contain. Equivalent to the
 | 
			
		||||
        :attr:`~horizon.tables.MultiTableView.table_classes` attribute on
 | 
			
		||||
        :class:`~horizon.tables.MultiTableView`. For each tuple you
 | 
			
		||||
        need to define a corresponding ``get_{{ formset_name }}_data`` method
 | 
			
		||||
        as with :class:`~horizon.tables.MultiTableView`.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    formset_definitions = None
 | 
			
		||||
 | 
			
		||||
    def __init__(self, workflow):
 | 
			
		||||
        super(FormsetStep, self).__init__(workflow)
 | 
			
		||||
        if not self.formset_definitions:
 | 
			
		||||
            class_name = self.__class__.__name__
 | 
			
		||||
            raise NotImplementedError("You must define a formset_definitions "
 | 
			
		||||
                                      "attribute on %s" % class_name)
 | 
			
		||||
        self._formsets = {}
 | 
			
		||||
        self._formset_data_loaded = False
 | 
			
		||||
 | 
			
		||||
    def prepare_action_context(self, request, context):
 | 
			
		||||
        """
 | 
			
		||||
        Passes the formsets to the action for validation and data extraction.
 | 
			
		||||
        """
 | 
			
		||||
        formsets = self.load_formset_data(request.POST or None)
 | 
			
		||||
        context['_formsets'] = formsets
 | 
			
		||||
        return context
 | 
			
		||||
 | 
			
		||||
    def load_formset_data(self, post_data=None):
 | 
			
		||||
        """
 | 
			
		||||
        Calls the ``get_{{ formset_name }}_data`` methods for each formse
 | 
			
		||||
        class and creates the formsets. Returns a dictionary with the formsets.
 | 
			
		||||
 | 
			
		||||
        This is called from prepare_action_context.
 | 
			
		||||
        """
 | 
			
		||||
        # Mark our data as loaded so we can check it later.
 | 
			
		||||
        self._formset_data_loaded = True
 | 
			
		||||
 | 
			
		||||
        for formset_name, formset_class in self.formset_definitions:
 | 
			
		||||
            # Fetch the data function.
 | 
			
		||||
            func_name = "get_%s_data" % formset_name
 | 
			
		||||
            data_func = getattr(self, func_name, None)
 | 
			
		||||
            if data_func is None:
 | 
			
		||||
                cls_name = self.__class__.__name__
 | 
			
		||||
                raise NotImplementedError("You must define a %s method "
 | 
			
		||||
                                          "on %s." % (func_name, cls_name))
 | 
			
		||||
            # Load the data and create the formsets.
 | 
			
		||||
            initial = data_func()
 | 
			
		||||
            self._formsets[formset_name] = formset_class(
 | 
			
		||||
                data=post_data,
 | 
			
		||||
                initial=initial,
 | 
			
		||||
            )
 | 
			
		||||
        return self._formsets
 | 
			
		||||
 | 
			
		||||
    def render(self):
 | 
			
		||||
        """ Renders the step. """
 | 
			
		||||
        step_template = template.loader.get_template(self.template_name)
 | 
			
		||||
        extra_context = {"form": self.action,
 | 
			
		||||
                         "step": self}
 | 
			
		||||
        if issubclass(self.__class__, FormsetStep):
 | 
			
		||||
            extra_context.update(self.get_context_data(self.workflow.request))
 | 
			
		||||
        context = template.RequestContext(self.workflow.request, extra_context)
 | 
			
		||||
        return step_template.render(context)
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, request):
 | 
			
		||||
        """
 | 
			
		||||
        Adds a ``{{ formset_name }}_formset`` item to the context for each
 | 
			
		||||
        formset in the ``formset_definitions`` attribute.
 | 
			
		||||
 | 
			
		||||
        If only one table class is provided, a shortcut ``formset`` context
 | 
			
		||||
        variable is also added containing the single formset.
 | 
			
		||||
        """
 | 
			
		||||
        context = {}
 | 
			
		||||
        # The data should have been loaded by now.
 | 
			
		||||
        if not self._formset_data_loaded:
 | 
			
		||||
            raise RuntimeError("You must load the data with load_formset_data"
 | 
			
		||||
                               "before displaying the step.")
 | 
			
		||||
        for formset_name, formset in self._formsets.iteritems():
 | 
			
		||||
            context["%s_formset" % formset_name] = formset
 | 
			
		||||
        # If there's only one formset class, add a shortcut name as well.
 | 
			
		||||
        if len(self._formsets) == 1:
 | 
			
		||||
            context["formset"] = formset
 | 
			
		||||
        return context
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# FIXME: TableStep
 | 
			
		||||
class TableStep(horizon.workflows.Step):
 | 
			
		||||
    """
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user