diff --git a/horizon/tables/actions.py b/horizon/tables/actions.py index af4f4113bd..b2e0473a19 100644 --- a/horizon/tables/actions.py +++ b/horizon/tables/actions.py @@ -687,7 +687,10 @@ class BatchAction(Action): def _allowed(self, request, datum=None): # Override the default internal action method to prevent batch # actions from appearing on tables with no data. - if not self.table.data and not datum: + # Updating single row of table by ajax prove that there is one + # data at least. + action = request.GET.get('action') + if action != 'row_update' and not self.table.data and not datum: return False return super(BatchAction, self)._allowed(request, datum) diff --git a/horizon/tables/base.py b/horizon/tables/base.py index a9bce95dad..c7344a1ed8 100644 --- a/horizon/tables/base.py +++ b/horizon/tables/base.py @@ -1105,6 +1105,8 @@ class DataTableOptions(object): # Set self.filter if we have any FilterActions filter_actions = [action for action in self.table_actions if issubclass(action, FilterAction)] + batch_actions = [action for action in self.table_actions if + issubclass(action, BatchAction)] if len(filter_actions) > 1: raise NotImplementedError("Multiple filter actions are not " "currently supported.") @@ -1137,7 +1139,7 @@ class DataTableOptions(object): len(self.row_actions) > 0) self.multi_select = getattr(options, 'multi_select', - len(self.table_actions) > 0) + len(batch_actions) > 0) # Set runtime table defaults; not configurable. self.has_prev_data = False @@ -1301,6 +1303,16 @@ class DataTable(object): self.needs_summary_row = any([col.summation for col in self.columns.values()]) + # For multi-process, we need to set the multi_column to be visible + # or hidden each time. + # Example: first process the multi_column visible but second + # process the column is hidden. Updating row by ajax will + # make the bug#1799151 + if request.GET.get('action') == 'row_update': + bound_actions = self.get_table_actions() + batch_actions = [action for action in bound_actions + if isinstance(action, BatchAction)] + self.set_multiselect_column_visibility(bool(batch_actions)) def __str__(self): return six.text_type(self._meta.verbose_name) @@ -1570,7 +1582,7 @@ class DataTable(object): if self._meta.table_actions_menu_label: extra_context['table_actions_menu_label'] = \ self._meta.table_actions_menu_label - self.set_multiselect_column_visibility(len(batch_actions) > 0) + self.set_multiselect_column_visibility(bool(batch_actions)) return table_actions_template.render(extra_context, self.request) def render_row_actions(self, datum, row=False): diff --git a/horizon/test/unit/tables/test_tables.py b/horizon/test/unit/tables/test_tables.py index f56737f8c6..bb8c49a07b 100644 --- a/horizon/test/unit/tables/test_tables.py +++ b/horizon/test/unit/tables/test_tables.py @@ -500,7 +500,7 @@ class DataTableTests(test.TestCase): class TempTable(MyTable): class Meta(object): columns = ('id',) - table_actions = (MyFilterAction, MyAction,) + table_actions = (MyFilterAction, MyAction, MyBatchAction) row_actions = (MyAction, MyLinkAction,) actions_column = False self.table = TempTable(self.request, TEST_DATA) @@ -528,7 +528,7 @@ class DataTableTests(test.TestCase): class TempTable(MyTable): class Meta(object): columns = ('id',) - table_actions = (MyFilterAction, MyAction,) + table_actions = (MyFilterAction, MyAction, MyBatchAction) self.table = TempTable(self.request, TEST_DATA) self.assertQuerysetEqual(self.table.columns.values(), ['', @@ -550,7 +550,7 @@ class DataTableTests(test.TestCase): class Meta(object): name = "temp_table" - table_actions = (MyFilterAction, MyAction,) + table_actions = (MyFilterAction, MyAction, MyBatchAction) row_actions = (MyAction, MyLinkAction,) self.table = TempTable(self.request, TEST_DATA) diff --git a/openstack_dashboard/dashboards/project/instances/tests.py b/openstack_dashboard/dashboards/project/instances/tests.py index 9338e89622..8a4ac2033e 100644 --- a/openstack_dashboard/dashboards/project/instances/tests.py +++ b/openstack_dashboard/dashboards/project/instances/tests.py @@ -5181,7 +5181,8 @@ class InstanceAjaxTests(helpers.TestCase, InstanceTestHelperMixin): @helpers.create_mocks({api.nova: ("server_get", "flavor_get", "extension_supported", - "is_feature_available"), + "is_feature_available", + "tenant_absolute_limits"), api.network: ('servers_update_addresses',)}) def test_row_update(self): server = self.servers.first() @@ -5220,7 +5221,8 @@ class InstanceAjaxTests(helpers.TestCase, InstanceTestHelperMixin): @helpers.create_mocks({api.nova: ("server_get", "flavor_get", 'is_feature_available', - "extension_supported"), + "extension_supported", + "tenant_absolute_limits"), api.network: ('servers_update_addresses',)}) def test_row_update_instance_error(self): server = self.servers.first() @@ -5280,7 +5282,8 @@ class InstanceAjaxTests(helpers.TestCase, InstanceTestHelperMixin): @helpers.create_mocks({api.nova: ("server_get", "flavor_get", 'is_feature_available', - "extension_supported"), + "extension_supported", + "tenant_absolute_limits"), api.network: ('servers_update_addresses',)}) def test_row_update_flavor_not_found(self): server = self.servers.first() diff --git a/openstack_dashboard/dashboards/project/volumes/tests.py b/openstack_dashboard/dashboards/project/volumes/tests.py index c2050e6bb4..8bbcabcf3e 100644 --- a/openstack_dashboard/dashboards/project/volumes/tests.py +++ b/openstack_dashboard/dashboards/project/volumes/tests.py @@ -1137,7 +1137,9 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.assertEqual(res.status_code, 200) mock_get.assert_called_once_with(test.IsHttpRequest(), volume.id) - mock_limits.assert_called_once() + self.assert_mock_multiple_calls_with_same_arguments( + mock_limits, 2, + mock.call(test.IsHttpRequest())) self.assertNotContains(res, 'Delete Volume') self.assertNotContains(res, 'delete') @@ -1275,7 +1277,9 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): content) self.assertNotIn('disabled', content) mock_get.assert_called_once_with(test.IsHttpRequest(), volume.id) - mock_limits.assert_called_once() + self.assert_mock_multiple_calls_with_same_arguments( + mock_limits, 2, + mock.call(test.IsHttpRequest())) @mock.patch.object(cinder, 'tenant_absolute_limits') @mock.patch.object(cinder, 'volume_get') @@ -1306,7 +1310,9 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.assertIn('disabled', content, 'The create snapshot button should be disabled') mock_get.assert_called_once_with(test.IsHttpRequest(), volume.id) - mock_limits.assert_called_once() + self.assert_mock_multiple_calls_with_same_arguments( + mock_limits, 2, + mock.call(test.IsHttpRequest())) @test.create_mocks({ api.nova: ['server_list'], @@ -1504,7 +1510,9 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.assertEqual(volume.name, volume.id) mock_get.assert_called_once_with(test.IsHttpRequest(), volume.id) - mock_limits.assert_called_once() + self.assert_mock_multiple_calls_with_same_arguments( + mock_limits, 2, + mock.call(test.IsHttpRequest())) @test.create_mocks({ api.nova: ['server_get'], @@ -1748,7 +1756,9 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase): self.assertContains(res, 'retype') mock_get.assert_called_once_with(test.IsHttpRequest(), volume.id) - mock_limits.assert_called_once() + self.assert_mock_multiple_calls_with_same_arguments( + mock_limits, 2, + mock.call(test.IsHttpRequest())) @test.create_mocks({ cinder: ['volume_type_list',