From c9befb3327ccea97843cefe9222afa14c8da4a95 Mon Sep 17 00:00:00 2001 From: Jordan OMara Date: Tue, 11 Feb 2014 18:04:45 -0500 Subject: [PATCH] Add pagination to stack list Implements: blueprint heat-stack-list-paging Change-Id: I87ebd7feaeddcfe7fe76c92b075c53de7016846b --- openstack_dashboard/api/heat.py | 26 +++++++- .../dashboards/project/stacks/tables.py | 1 + .../dashboards/project/stacks/tests.py | 60 ++++++++++++++++--- .../dashboards/project/stacks/views.py | 20 +++++-- .../test/api_tests/heat_tests.py | 9 ++- .../test/test_data/heat_data.py | 60 +++++++++---------- 6 files changed, 129 insertions(+), 47 deletions(-) diff --git a/openstack_dashboard/api/heat.py b/openstack_dashboard/api/heat.py index 8411c400a4..071066a73f 100644 --- a/openstack_dashboard/api/heat.py +++ b/openstack_dashboard/api/heat.py @@ -16,6 +16,7 @@ import logging from django.conf import settings from heatclient import client as heat_client +from horizon.utils import functions as utils from openstack_dashboard.api import base LOG = logging.getLogger(__name__) @@ -52,8 +53,29 @@ def heatclient(request, password=None): return client -def stacks_list(request): - return [stack for stack in heatclient(request).stacks.list()] +def stacks_list(request, marker=None, paginate=False): + limit = getattr(settings, 'API_RESULT_LIMIT', 1000) + page_size = utils.get_page_size(request) + + if paginate: + request_size = page_size + 1 + else: + request_size = limit + + kwargs = {} + if marker: + kwargs['marker'] = marker + + stacks_iter = heatclient(request).stacks.list(limit=request_size, + **kwargs) + + has_more_data = False + stacks = list(stacks_iter) + if paginate: + if len(stacks) > page_size: + stacks.pop() + has_more_data = True + return (stacks, has_more_data) def stack_delete(request, stack_id): diff --git a/openstack_dashboard/dashboards/project/stacks/tables.py b/openstack_dashboard/dashboards/project/stacks/tables.py index 02da559130..4e2dde7725 100644 --- a/openstack_dashboard/dashboards/project/stacks/tables.py +++ b/openstack_dashboard/dashboards/project/stacks/tables.py @@ -109,6 +109,7 @@ class StacksTable(tables.DataTable): class Meta: name = "stacks" verbose_name = _("Stacks") + pagination_param = 'stack_marker' status_columns = ["status", ] row_class = StacksUpdateRow table_actions = (LaunchStack, DeleteStack,) diff --git a/openstack_dashboard/dashboards/project/stacks/tests.py b/openstack_dashboard/dashboards/project/stacks/tests.py index 4c364c8597..239ae9b89b 100644 --- a/openstack_dashboard/dashboards/project/stacks/tests.py +++ b/openstack_dashboard/dashboards/project/stacks/tests.py @@ -14,9 +14,11 @@ import json +from django.conf import settings from django.core import exceptions from django.core.urlresolvers import reverse from django import http +from django.test.utils import override_settings # noqa from mox import IsA # noqa @@ -25,6 +27,7 @@ from openstack_dashboard.test import helpers as test from openstack_dashboard.dashboards.project.stacks import forms from openstack_dashboard.dashboards.project.stacks import mappings +from openstack_dashboard.dashboards.project.stacks import tables INDEX_URL = reverse('horizon:project:stacks:index') @@ -93,20 +96,59 @@ class MappingsTests(test.TestCase): class StackTests(test.TestCase): + @override_settings(API_RESULT_PAGE_SIZE=2) @test.create_stubs({api.heat: ('stacks_list',)}) - def test_index(self): - stacks = self.stacks.list() + def test_index_paginated(self): + stacks = self.stacks.list()[:5] + # import pdb; pdb.set_trace() - api.heat.stacks_list(IsA(http.HttpRequest)) \ - .AndReturn(stacks) + api.heat.stacks_list(IsA(http.HttpRequest), + marker=None, + paginate=True) \ + .AndReturn([stacks, + True]) + api.heat.stacks_list(IsA(http.HttpRequest), + marker=None, + paginate=True) \ + .AndReturn([stacks[:2], + True]) + api.heat.stacks_list(IsA(http.HttpRequest), + marker=stacks[2].id, + paginate=True) \ + .AndReturn([stacks[2:4], + True]) + api.heat.stacks_list(IsA(http.HttpRequest), + marker=stacks[4].id, + paginate=True) \ + .AndReturn([stacks[4:], + True]) self.mox.ReplayAll() - res = self.client.get(INDEX_URL) - + url = reverse('horizon:project:stacks:index') + res = self.client.get(url) + # get all + self.assertEqual(len(res.context['stacks_table'].data), + len(stacks)) self.assertTemplateUsed(res, 'project/stacks/index.html') - self.assertIn('table', res.context) - resp_stacks = res.context['table'].data - self.assertEqual(len(resp_stacks), len(stacks)) + + res = self.client.get(url) + # get first page with 2 items + self.assertEqual(len(res.context['stacks_table'].data), + settings.API_RESULT_PAGE_SIZE) + + url = "%s?%s=%s" % (reverse('horizon:project:stacks:index'), + tables.StacksTable._meta.pagination_param, stacks[2].id) + res = self.client.get(url) + # get second page (items 2-4) + self.assertEqual(len(res.context['stacks_table'].data), + settings.API_RESULT_PAGE_SIZE) + + url = "%s?%s=%s" % (reverse('horizon:project:stacks:index'), + tables.StacksTable._meta.pagination_param, stacks[4].id) + res = self.client.get(url) + # get third page (item 5) + self.assertEqual(len(res.context['stacks_table'].data), + 1) @test.create_stubs({api.heat: ('stack_create', 'template_validate')}) def test_launch_stack(self): diff --git a/openstack_dashboard/dashboards/project/stacks/views.py b/openstack_dashboard/dashboards/project/stacks/views.py index 31096a20fc..d0fd51a07d 100644 --- a/openstack_dashboard/dashboards/project/stacks/views.py +++ b/openstack_dashboard/dashboards/project/stacks/views.py @@ -46,13 +46,25 @@ class IndexView(tables.DataTableView): table_class = project_tables.StacksTable template_name = 'project/stacks/index.html' + def __init__(self, *args, **kwargs): + super(IndexView, self).__init__(*args, **kwargs) + self._more = None + + def has_more_data(self, table): + return self._more + def get_data(self): - request = self.request + stacks = [] + marker = self.request.GET.get( + project_tables.StacksTable._meta.pagination_param) try: - stacks = api.heat.stacks_list(self.request) + stacks, self._more = api.heat.stacks_list(self.request, + marker=marker, + paginate=True) except Exception: - exceptions.handle(request, _('Unable to retrieve stack list.')) - stacks = [] + self._more = False + msg = _('Unable to retrieve stack list.') + exceptions.handle(self.request, msg) return stacks diff --git a/openstack_dashboard/test/api_tests/heat_tests.py b/openstack_dashboard/test/api_tests/heat_tests.py index e1ae6fcd21..b2d5e0a558 100644 --- a/openstack_dashboard/test/api_tests/heat_tests.py +++ b/openstack_dashboard/test/api_tests/heat_tests.py @@ -12,6 +12,9 @@ # License for the specific language governing permissions and limitations # under the License. +from django.conf import settings +from django.test.utils import override_settings # noqa + from openstack_dashboard import api from openstack_dashboard.test import helpers as test @@ -19,13 +22,15 @@ from openstack_dashboard.test import helpers as test class HeatApiTests(test.APITestCase): def test_stack_list(self): api_stacks = self.stacks.list() + limit = getattr(settings, 'API_RESULT_LIMIT', 1000) heatclient = self.stub_heatclient() heatclient.stacks = self.mox.CreateMockAnything() - heatclient.stacks.list().AndReturn(iter(api_stacks)) + heatclient.stacks.list(limit=limit).AndReturn(iter(api_stacks)) self.mox.ReplayAll() - stacks = api.heat.stacks_list(self.request) + stacks, has_more = api.heat.stacks_list(self.request) self.assertItemsEqual(stacks, api_stacks) + self.assertFalse(has_more) def test_template_get(self): api_stacks = self.stacks.list() diff --git a/openstack_dashboard/test/test_data/heat_data.py b/openstack_dashboard/test/test_data/heat_data.py index d550e7084b..2bbd64db53 100644 --- a/openstack_dashboard/test/test_data/heat_data.py +++ b/openstack_dashboard/test/test_data/heat_data.py @@ -329,36 +329,36 @@ def data(TEST): TEST.stack_templates = utils.TestDataContainer() TEST.stack_environments = utils.TestDataContainer() - # Stacks - stack1 = { - "description": "No description", - "links": [{ - "href": "http://192.168.1.70:8004/v1/" - "051c727ee67040d6a7b7812708485a97/" - "stacks/stack-1211-38/" - "05b4f39f-ea96-4d91-910c-e758c078a089", - "rel": "self" - }], - "parameters": { - 'DBUsername': '******', - 'InstanceType': 'm1.small', - 'AWS::StackId': - 'arn:openstack:heat::2ce287:stacks/teststack/88553ec', - 'DBRootPassword': '******', - 'AWS::StackName': 'teststack', - 'DBPassword': '******', - 'AWS::Region': 'ap-southeast-1', - 'DBName': u'wordpress' - }, - "stack_status_reason": "Stack successfully created", - "stack_name": "stack-test", - "creation_time": "2013-04-22T00:11:39Z", - "updated_time": "null", - "stack_status": "CREATE_COMPLETE", - "id": "05b4f39f-ea96-4d91-910c-e758c078a089" - } - stack = stacks.Stack(stacks.StackManager(None), stack1) - TEST.stacks.add(stack) + for i in range(10): + stack_data = { + "description": "No description", + "links": [{ + "href": "http://192.168.1.70:8004/v1/" + "051c727ee67040d6a7b7812708485a97/" + "stacks/stack-1211-38/" + "05b4f39f-ea96-4d91-910c-e758c078a089", + "rel": "self" + }], + "parameters": { + 'DBUsername': '******', + 'InstanceType': 'm1.small', + 'AWS::StackId': + 'arn:openstack:heat::2ce287:stacks/teststack/88553ec', + 'DBRootPassword': '******', + 'AWS::StackName': "teststack{0}".format(i), + 'DBPassword': '******', + 'AWS::Region': 'ap-southeast-1', + 'DBName': u'wordpress' + }, + "stack_status_reason": "Stack successfully created", + "stack_name": "stack-test{0}".format(i), + "creation_time": "2013-04-22T00:11:39Z", + "updated_time": "2013-04-22T00:11:39Z", + "stack_status": "CREATE_COMPLETE", + "id": "05b4f39f-ea96-4d91-910c-e758c078a089{0}".format(i) + } + stack = stacks.Stack(stacks.StackManager(None), stack_data) + TEST.stacks.add(stack) TEST.stack_templates.add(Template(TEMPLATE, VALIDATE)) TEST.stack_environments.add(Environment(ENVIRONMENT))