From fb9399a1c53b895177fb93e74a2fb3981870e413 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Thu, 26 Sep 2013 15:54:10 +0200 Subject: [PATCH] Merge FormsetStep and TableStep. The FormsetStep is no longer needed, TableStep is used instead, with a special version of DataTable, FormsetDataTable, that apart from all the normal DataTable features also generates a Django Formset with all of its data, and displays it the same way the FormsetStep did. One issue is not addressed: * The user-provided data in the forms can be lost if that row gets updated by ajax autoupdate. Change-Id: I5264b484f61212c21102739b803d40d9d31ae2cc --- .../resource_management/racks/tables.py | 2 +- .../resource_classes/forms.py | 9 + .../resource_classes/tables.py | 43 +++- .../resource_classes/tests.py | 44 +++- .../resource_classes/workflows.py | 59 +++-- .../resource_classes/_formset.html | 128 ---------- ..._resource_class_info_and_flavors_step.html | 2 +- .../infrastructure/less/infrastructure.less | 8 +- .../templates/formset_table/_row.html | 27 +++ .../templates/formset_table/_table.html | 28 +++ .../templates/infrastructure/_scripts.html | 2 +- tuskar_ui/tables.py | 225 ++++++++++++++---- tuskar_ui/test/formset_table_tests.py | 58 +++++ tuskar_ui/workflows.py | 106 +-------- 14 files changed, 424 insertions(+), 317 deletions(-) delete mode 100644 tuskar_ui/infrastructure/resource_management/templates/resource_management/resource_classes/_formset.html create mode 100644 tuskar_ui/infrastructure/templates/formset_table/_row.html create mode 100644 tuskar_ui/infrastructure/templates/formset_table/_table.html create mode 100644 tuskar_ui/test/formset_table_tests.py diff --git a/tuskar_ui/infrastructure/resource_management/racks/tables.py b/tuskar_ui/infrastructure/resource_management/racks/tables.py index e5c005dac..651a94ac1 100644 --- a/tuskar_ui/infrastructure/resource_management/racks/tables.py +++ b/tuskar_ui/infrastructure/resource_management/racks/tables.py @@ -96,7 +96,7 @@ class RacksTable(tables.DataTable): 'vm_capacity', verbose_name=_("Usage"), filters=(lambda vm_capacity: - (vm_capacity.value and + (vm_capacity and vm_capacity.value and "%s %%" % int(round((100 / float(vm_capacity.value)) * vm_capacity.usage, 0))) or None,)) diff --git a/tuskar_ui/infrastructure/resource_management/resource_classes/forms.py b/tuskar_ui/infrastructure/resource_management/resource_classes/forms.py index f0dfbc6bf..cb82435e3 100644 --- a/tuskar_ui/infrastructure/resource_management/resource_classes/forms.py +++ b/tuskar_ui/infrastructure/resource_management/resource_classes/forms.py @@ -13,6 +13,7 @@ # under the License. from django.core import urlresolvers +import django.forms from django.utils.translation import ugettext_lazy as _ # noqa from horizon import exceptions @@ -64,3 +65,11 @@ class DeleteCommand(object): redirect = urlresolvers.reverse( 'horizon:infrastructure:resource_management:index') exceptions.handle(self.request, self.msg, redirect=redirect) + + +class SelectRack(django.forms.Form): + id = django.forms.IntegerField(widget=django.forms.HiddenInput()) + selected = django.forms.BooleanField(required=False) + + +SelectRackFormset = django.forms.formsets.formset_factory(SelectRack, extra=0) diff --git a/tuskar_ui/infrastructure/resource_management/resource_classes/tables.py b/tuskar_ui/infrastructure/resource_management/resource_classes/tables.py index ad604a147..572255fc4 100644 --- a/tuskar_ui/infrastructure/resource_management/resource_classes/tables.py +++ b/tuskar_ui/infrastructure/resource_management/resource_classes/tables.py @@ -22,9 +22,13 @@ from horizon import exceptions from horizon import tables from tuskar_ui import api as tuskar +from tuskar_ui.infrastructure.resource_management.flavors\ + import forms as flavors_forms from tuskar_ui.infrastructure.resource_management.racks\ import tables as racks_tables from tuskar_ui.infrastructure.resource_management import resource_classes +from tuskar_ui.infrastructure.resource_management.resource_classes\ + import forms import tuskar_ui.tables @@ -98,15 +102,28 @@ class RacksFilterAction(tables.FilterAction): class RacksTable(racks_tables.RacksTable): + class Meta: + name = "racks" + verbose_name = _("Racks") + table_actions = (RacksFilterAction,) - multi_select_name = "racks_object_ids" + +class RacksFormsetTable(tuskar_ui.tables.FormsetDataTableMixin, RacksTable): + formset_class = forms.SelectRackFormset class Meta: name = "racks" verbose_name = _("Racks") - multi_select = True + multi_select = False table_actions = (RacksFilterAction,) - row_class = tuskar_ui.tables.MultiselectRow + + def __init__(self, *args, **kwargs): + # Adding a column at the left of the table. + selected = tables.Column('selected', verbose_name="", sortable=False) + selected.classes.append('narrow') + selected.table = self + self._columns.insert(0, 'selected', selected) + super(RacksFormsetTable, self).__init__(*args, **kwargs) class UpdateRacksClass(tables.LinkAction): @@ -136,7 +153,7 @@ class UpdateFlavorsClass(tables.LinkAction): return "%s?step=%s" % ( urlresolvers.reverse( url, - args=(self.table.kwargs['resource_class_id'],)), + args=(self.table.kwargs.get('resource_class_id'),)), resource_classes.workflows.ResourceClassInfoAndFlavorsAction.slug) @@ -187,3 +204,21 @@ class FlavorsTable(tables.DataTable): name = "flavors" verbose_name = _("Flavors") table_actions = (FlavorsFilterAction, UpdateFlavorsClass) + + +class FlavorsFormsetTable(tuskar_ui.tables.FormsetDataTableMixin, + FlavorsTable): + + name = tables.Column( + 'name', + verbose_name=_('Flavor Name'), + filters=(lambda n: (n or '.').split('.')[1],), + ) + DELETE = tables.Column('DELETE', verbose_name=_("Delete")) + formset_class = flavors_forms.FlavorFormset + + class Meta: + name = "flavors" + verbose_name = _("Flavors") + table_actions = () + multi_select = False diff --git a/tuskar_ui/infrastructure/resource_management/resource_classes/tests.py b/tuskar_ui/infrastructure/resource_management/resource_classes/tests.py index b2504f6ab..0cce7dc2e 100644 --- a/tuskar_ui/infrastructure/resource_management/resource_classes/tests.py +++ b/tuskar_ui/infrastructure/resource_management/resource_classes/tests.py @@ -46,6 +46,7 @@ class ResourceClassViewTests(test.BaseAdminViewTests): @test.create_stubs({ tuskar.ResourceClass: ('list', 'create', 'set_racks'), + tuskar.Rack: ('list',), }) def test_create_resource_class_post(self): new_resource_class = self.tuskar_resource_classes.first() @@ -54,6 +55,8 @@ class ResourceClassViewTests(test.BaseAdminViewTests): add_racks_ids = [] + tuskar.Rack.list( + mox.IsA(http.request.HttpRequest), True).AndReturn([]) tuskar.ResourceClass.list( mox.IsA(http.request.HttpRequest)).AndReturn( self.tuskar_resource_classes.list()) @@ -76,6 +79,9 @@ class ResourceClassViewTests(test.BaseAdminViewTests): 'flavors-TOTAL_FORMS': 0, 'flavors-INITIAL_FORMS': 0, 'flavors-MAX_NUM_FORMS': 1000, + 'racks-TOTAL_FORMS': 0, + 'racks-INITIAL_FORMS': 0, + 'racks-MAX_NUM_FORMS': 1000, } res = self.client.post(url, form_data) self.assertNoFormErrors(res) @@ -89,12 +95,15 @@ class ResourceClassViewTests(test.BaseAdminViewTests): @test.create_stubs({ tuskar.ResourceClass: ('list', 'create', 'set_racks'), + tuskar.Rack: ('list',), }) def test_create_resource_class_post_exception(self): new_resource_class = self.tuskar_resource_classes.first() new_unique_name = "unique_name_for_sure" new_flavors = [] + tuskar.Rack.list( + mox.IsA(http.request.HttpRequest), True).AndReturn([]) tuskar.ResourceClass.list( mox.IsA(http.request.HttpRequest)).AndReturn( self.tuskar_resource_classes.list()) @@ -115,6 +124,9 @@ class ResourceClassViewTests(test.BaseAdminViewTests): 'flavors-TOTAL_FORMS': 0, 'flavors-INITIAL_FORMS': 0, 'flavors-MAX_NUM_FORMS': 1000, + 'racks-TOTAL_FORMS': 0, + 'racks-INITIAL_FORMS': 0, + 'racks-MAX_NUM_FORMS': 1000, } res = self.client.post(url, form_data) self.assertRedirectsNoFollow(res, @@ -180,13 +192,19 @@ class ResourceClassViewTests(test.BaseAdminViewTests): @test.create_stubs({ tuskar.ResourceClass: ('get', 'list', 'update', 'set_racks', - 'list_flavors') + 'list_flavors', 'all_racks', 'racks_ids'), + tuskar.Rack: ('list',), }) def test_edit_resource_class_post(self): resource_class = self.tuskar_resource_classes.first() add_racks_ids = [] + tuskar.ResourceClass.get( + mox.IsA(http.HttpRequest), resource_class.id).AndReturn( + resource_class) + tuskar.ResourceClass.all_racks = [] + tuskar.ResourceClass.racks_ids = [] tuskar.ResourceClass.get( mox.IsA(http.HttpRequest), resource_class.id).AndReturn( resource_class) @@ -214,6 +232,9 @@ class ResourceClassViewTests(test.BaseAdminViewTests): 'flavors-TOTAL_FORMS': 0, 'flavors-INITIAL_FORMS': 0, 'flavors-MAX_NUM_FORMS': 1000, + 'racks-TOTAL_FORMS': 0, + 'racks-INITIAL_FORMS': 0, + 'racks-MAX_NUM_FORMS': 1000, } url = urlresolvers.reverse( 'horizon:infrastructure:resource_management:resource_classes:' @@ -429,13 +450,18 @@ class ResourceClassViewTests(test.BaseAdminViewTests): @test.create_stubs({ tuskar.ResourceClass: ('get', 'list', 'update', 'set_racks', - 'list_flavors') + 'list_flavors', 'all_racks', 'racks_ids') }) def test_detail_edit_racks_post(self): resource_class = self.tuskar_resource_classes.first() add_racks_ids = [] + tuskar.ResourceClass.get( + mox.IsA(http.HttpRequest), resource_class.id).AndReturn( + resource_class) + tuskar.ResourceClass.all_racks = [] + tuskar.ResourceClass.racks_ids = [] tuskar.ResourceClass.get( mox.IsA(http.HttpRequest), resource_class.id).AndReturn( resource_class) @@ -463,6 +489,9 @@ class ResourceClassViewTests(test.BaseAdminViewTests): 'flavors-TOTAL_FORMS': 0, 'flavors-INITIAL_FORMS': 0, 'flavors-MAX_NUM_FORMS': 1000, + 'racks-TOTAL_FORMS': 0, + 'racks-INITIAL_FORMS': 0, + 'racks-MAX_NUM_FORMS': 1000, } url = urlresolvers.reverse( 'horizon:infrastructure:resource_management:resource_classes:' @@ -517,7 +546,8 @@ class ResourceClassViewTests(test.BaseAdminViewTests): @test.create_stubs({ tuskar.ResourceClass: ('get', 'list', 'update', 'set_racks', - 'list_flavors') + 'list_flavors', 'all_racks', 'racks_ids'), + tuskar.Rack: ('list',), }) def test_detail_edit_flavors_post(self): resource_class = self.tuskar_resource_classes.first() @@ -527,6 +557,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_racks = [] + tuskar.ResourceClass.racks_ids = [] tuskar.ResourceClass.get( mox.IsA(http.HttpRequest), resource_class.id).AndReturn( resource_class) @@ -551,6 +586,9 @@ class ResourceClassViewTests(test.BaseAdminViewTests): 'flavors-TOTAL_FORMS': 0, 'flavors-INITIAL_FORMS': 0, 'flavors-MAX_NUM_FORMS': 1000, + 'racks-TOTAL_FORMS': 0, + 'racks-INITIAL_FORMS': 0, + 'racks-MAX_NUM_FORMS': 1000, } url = urlresolvers.reverse( 'horizon:infrastructure:resource_management:resource_classes:' diff --git a/tuskar_ui/infrastructure/resource_management/resource_classes/workflows.py b/tuskar_ui/infrastructure/resource_management/resource_classes/workflows.py index e8fd99758..04f01bfc2 100644 --- a/tuskar_ui/infrastructure/resource_management/resource_classes/workflows.py +++ b/tuskar_ui/infrastructure/resource_management/resource_classes/workflows.py @@ -77,13 +77,15 @@ class ResourceClassInfoAndFlavorsAction(workflows.Action): ' another resource class.') % name ) - formset = self.initial.get('_formsets', {}).get('flavors') - if formset: + table = self.initial.get('_tables', {}).get('flavors') + if table: + formset = table.get_formset() if formset.is_valid(): cleaned_data['flavors'] = [form.cleaned_data for form in formset if form.cleaned_data - and not form.cleaned_data['DELETE']] + and not + form.cleaned_data.get('DELETE')] else: raise forms.ValidationError( _('Errors in the flavors list.'), @@ -96,10 +98,8 @@ class ResourceClassInfoAndFlavorsAction(workflows.Action): "settings and add flavors to class.") -class CreateResourceClassInfoAndFlavors(tuskar_ui.workflows.FormsetStep): - formset_definitions = ( - ('flavors', flavors_forms.FlavorFormset), - ) +class CreateResourceClassInfoAndFlavors(tuskar_ui.workflows.TableStep): + table_classes = (tables.FlavorsFormsetTable,) action_class = ResourceClassInfoAndFlavorsAction template_name = 'infrastructure/resource_management/resource_classes/'\ @@ -120,34 +120,33 @@ class CreateResourceClassInfoAndFlavors(tuskar_ui.workflows.FormsetStep): flavors = [] exceptions.handle(self.workflow.request, _('Unable to retrieve resource flavors list.')) - flavors_data = [] - for flavor in flavors: - if '.' in flavor.name: - name = flavor.name.split('.', 1)[1] - else: - name = flavor.name - data = { - 'id': flavor.id, - 'name': name, - } - for capacity_name in flavors_forms.CAPACITIES: - capacity = getattr(flavor, capacity_name, None) - capacity_value = getattr(capacity, 'value', '') - # Make sure we don't have "None" in there - if capacity_value is None: - capacity_value = '' - data[capacity_name] = capacity_value - flavors_data.append(data) - return flavors_data + return flavors class RacksAction(workflows.Action): class Meta: name = _("Racks") + def clean(self): + cleaned_data = super(RacksAction, self).clean() + table = self.initial.get('_tables', {}).get('racks') + if table: + formset = table.get_formset() + if formset.is_valid(): + cleaned_data['racks_object_ids'] = [ + form.cleaned_data['id'] for form in formset + if form.cleaned_data and + form.cleaned_data.get('selected') and + not form.cleaned_data.get('DELETE')] + else: + raise forms.ValidationError( + _('Errors in the racks table.'), + ) + return cleaned_data + class CreateRacks(tuskar_ui.workflows.TableStep): - table_classes = (tables.RacksTable,) + table_classes = (tables.RacksFormsetTable,) action_class = RacksAction contributes = ("racks_object_ids") @@ -169,10 +168,10 @@ class CreateRacks(tuskar_ui.workflows.TableStep): resource_class = tuskar.ResourceClass.get( self.workflow.request, resource_class_id) - # TODO(lsmola ugly interface, rewrite) - self._tables['racks'].active_multi_select_values = \ - resource_class.racks_ids + selected_racks = resource_class.racks_ids racks = resource_class.all_racks + for rack in racks: + rack.selected = (rack.id in selected_racks) else: racks = tuskar.Rack.list(self.workflow.request, True) except Exception: diff --git a/tuskar_ui/infrastructure/resource_management/templates/resource_management/resource_classes/_formset.html b/tuskar_ui/infrastructure/resource_management/templates/resource_management/resource_classes/_formset.html deleted file mode 100644 index 8a2ccf201..000000000 --- a/tuskar_ui/infrastructure/resource_management/templates/resource_management/resource_classes/_formset.html +++ /dev/null @@ -1,128 +0,0 @@ -{% load i18n %} - -{{ formset.management_form }} -{% if formset.non_field_errors %} -
- {{ formset.non_field_errors }} -
-{% endif %} - - - - - {% for field in flavors_formset.0.visible_fields %} - - {% endfor %} - - - {% for form in formset %} - - {% for field in form.visible_fields %} - - {% endfor %} - - {% endfor %} - - - - -
- {{ field.field.label }} -
- {{ field }} - {% for error in field.errors %} - {{ error }} - {% endfor %} - {% if forloop.first %} - {% for field in form.hidden_fields %} - {{ field }} - {% for error in field.errors %} - {{ field.name }}: {{ error }} - {% endfor %} - {% endfor %} - {% if form.non_field_errors %} -
- {{ form.non_field_errors }} -
- {% endif %} - {% endif %} -
-
- - - diff --git a/tuskar_ui/infrastructure/resource_management/templates/resource_management/resource_classes/_resource_class_info_and_flavors_step.html b/tuskar_ui/infrastructure/resource_management/templates/resource_management/resource_classes/_resource_class_info_and_flavors_step.html index e6cb2eb79..c7f5059a0 100644 --- a/tuskar_ui/infrastructure/resource_management/templates/resource_management/resource_classes/_resource_class_info_and_flavors_step.html +++ b/tuskar_ui/infrastructure/resource_management/templates/resource_management/resource_classes/_resource_class_info_and_flavors_step.html @@ -13,7 +13,7 @@
- {% include 'infrastructure/resource_management/resource_classes/_formset.html' with formset=flavors_formset %} + {{ flavors_table.render }}