diff --git a/horizon/api/glance.py b/horizon/api/glance.py index 1f8f8cd318..cda7cb2975 100644 --- a/horizon/api/glance.py +++ b/horizon/api/glance.py @@ -25,6 +25,7 @@ import logging import urlparse from django.utils.decorators import available_attrs +from django.conf import settings from glance import client as glance_client from glance.common import exception as glance_exception @@ -122,8 +123,17 @@ def image_get_meta(request, image_id): @catch_glance_exception -def image_list_detailed(request): - return [Image(i) for i in glanceclient(request).get_images_detailed()] +def image_list_detailed(request, marker=None, filters=None): + filters = filters or {} + limit = getattr(settings, 'API_RESULT_LIMIT', 1000) + image_dicts = glanceclient(request).get_images_detailed(limit=limit + 1, + marker=marker, + filters=filters) + images = [Image(i) for i in image_dicts] + if(len(images) > limit): + return (images[0:-1], True) + else: + return (images, False) @catch_glance_exception @@ -134,9 +144,15 @@ def image_update(request, image_id, image_meta=None): @catch_glance_exception -def snapshot_list_detailed(request): - filters = {} - filters['property-image_type'] = 'snapshot' - filters['is_public'] = 'none' - return [Image(i) for i in glanceclient(request) - .get_images_detailed(filters=filters)] +def snapshot_list_detailed(request, marker=None, extra_filters=None): + filters = {'property-image_type': 'snapshot'} + filters.update(extra_filters or {}) + limit = getattr(settings, 'API_RESULT_LIMIT', 1000) + image_dicts = glanceclient(request).get_images_detailed(limit=limit + 1, + marker=marker, + filters=filters) + images = [Image(i) for i in image_dicts] + if(len(images) > limit): + return (images[0:-1], True) + else: + return (images, False) diff --git a/horizon/dashboards/nova/images_and_snapshots/images/tables.py b/horizon/dashboards/nova/images_and_snapshots/images/tables.py index df57e6f01c..b22827b62f 100644 --- a/horizon/dashboards/nova/images_and_snapshots/images/tables.py +++ b/horizon/dashboards/nova/images_and_snapshots/images/tables.py @@ -99,3 +99,4 @@ class ImagesTable(tables.DataTable): verbose_name = _("Images") table_actions = (DeleteImage,) row_actions = (LaunchImage, EditImage, DeleteImage) + pagination_param = "image_marker" diff --git a/horizon/dashboards/nova/images_and_snapshots/snapshots/tables.py b/horizon/dashboards/nova/images_and_snapshots/snapshots/tables.py index 13b19dd314..b8abedb7d5 100644 --- a/horizon/dashboards/nova/images_and_snapshots/snapshots/tables.py +++ b/horizon/dashboards/nova/images_and_snapshots/snapshots/tables.py @@ -35,3 +35,4 @@ class SnapshotsTable(ImagesTable): verbose_name = _("Instance Snapshots") table_actions = (DeleteSnapshot,) row_actions = (LaunchImage, EditImage, DeleteSnapshot) + pagination_param = "snapshot_marker" diff --git a/horizon/dashboards/nova/images_and_snapshots/tests.py b/horizon/dashboards/nova/images_and_snapshots/tests.py index 124fad570f..cb0944cf19 100644 --- a/horizon/dashboards/nova/images_and_snapshots/tests.py +++ b/horizon/dashboards/nova/images_and_snapshots/tests.py @@ -38,8 +38,13 @@ class ImagesAndSnapshotsTests(test.TestCase): snapshots = self.snapshots.list() self.mox.StubOutWithMock(api, 'image_list_detailed') self.mox.StubOutWithMock(api, 'snapshot_list_detailed') - api.image_list_detailed(IsA(http.HttpRequest)).AndReturn(images) - api.snapshot_list_detailed(IsA(http.HttpRequest)).AndReturn(snapshots) + self.mox.StubOutWithMock(api, 'volume_snapshot_list') + api.volume_snapshot_list(IsA(http.HttpRequest)) \ + .AndReturn(self.volumes.list()) + api.image_list_detailed(IsA(http.HttpRequest), + marker=None).AndReturn([images, False]) + api.snapshot_list_detailed(IsA(http.HttpRequest), + marker=None).AndReturn([snapshots, False]) self.mox.ReplayAll() res = self.client.get(INDEX_URL) @@ -53,9 +58,13 @@ class ImagesAndSnapshotsTests(test.TestCase): def test_index_no_images(self): self.mox.StubOutWithMock(api, 'snapshot_list_detailed') self.mox.StubOutWithMock(api, 'image_list_detailed') - api.image_list_detailed(IsA(http.HttpRequest)).AndReturn([]) - api.snapshot_list_detailed(IsA(http.HttpRequest)) \ - .AndReturn(self.snapshots.list()) + self.mox.StubOutWithMock(api, 'volume_snapshot_list') + api.volume_snapshot_list(IsA(http.HttpRequest)) \ + .AndReturn(self.volumes.list()) + api.image_list_detailed(IsA(http.HttpRequest), + marker=None).AndReturn([(), False]) + api.snapshot_list_detailed(IsA(http.HttpRequest), marker=None) \ + .AndReturn([self.snapshots.list(), False]) self.mox.ReplayAll() res = self.client.get(INDEX_URL) @@ -65,9 +74,13 @@ class ImagesAndSnapshotsTests(test.TestCase): self.mox.StubOutWithMock(api, 'image_list_detailed') self.mox.StubOutWithMock(api, 'snapshot_list_detailed') exc = glance_exception.ClientConnectionError('clientConnError') - api.image_list_detailed(IsA(http.HttpRequest)).AndRaise(exc) - api.snapshot_list_detailed(IsA(http.HttpRequest)) \ - .AndReturn(self.snapshots.list()) + self.mox.StubOutWithMock(api, 'volume_snapshot_list') + api.volume_snapshot_list(IsA(http.HttpRequest)) \ + .AndReturn(self.volumes.list()) + api.image_list_detailed(IsA(http.HttpRequest), + marker=None).AndRaise(exc) + api.snapshot_list_detailed(IsA(http.HttpRequest), marker=None) \ + .AndReturn([self.snapshots.list(), False]) self.mox.ReplayAll() res = self.client.get(INDEX_URL) @@ -86,9 +99,13 @@ class ImagesAndSnapshotsTests(test.TestCase): new_snapshots = [snapshot1, snapshot2] self.mox.StubOutWithMock(api, 'image_list_detailed') self.mox.StubOutWithMock(api, 'snapshot_list_detailed') - api.image_list_detailed(IsA(http.HttpRequest)).AndReturn(images) - api.snapshot_list_detailed(IsA(http.HttpRequest)).\ - AndReturn(new_snapshots) + self.mox.StubOutWithMock(api, 'volume_snapshot_list') + api.volume_snapshot_list(IsA(http.HttpRequest)) \ + .AndReturn(self.volumes.list()) + api.image_list_detailed(IsA(http.HttpRequest), + marker=None).AndReturn([images, False]) + api.snapshot_list_detailed(IsA(http.HttpRequest), marker=None) \ + .AndReturn([new_snapshots, False]) self.mox.ReplayAll() res = self.client.get(INDEX_URL) diff --git a/horizon/dashboards/nova/images_and_snapshots/views.py b/horizon/dashboards/nova/images_and_snapshots/views.py index 82d0d0b9c8..7b84e5c504 100644 --- a/horizon/dashboards/nova/images_and_snapshots/views.py +++ b/horizon/dashboards/nova/images_and_snapshots/views.py @@ -42,9 +42,16 @@ class IndexView(tables.MultiTableView): table_classes = (ImagesTable, SnapshotsTable, VolumeSnapshotsTable) template_name = 'nova/images_and_snapshots/index.html' + def has_more_data(self, table): + return getattr(self, "_more_%s" % table.name, False) + def get_images_data(self): + marker = self.request.GET.get(ImagesTable._meta.pagination_param, None) try: - all_images = api.image_list_detailed(self.request) + # FIXME(gabriel): The paging is going to be strange here due to + # our filtering after the fact. + all_images, _more_images = api.image_list_detailed(self.request, + marker=marker) images = [im for im in all_images if im['container_format'] not in ['aki', 'ari'] and getattr(im.properties, "image_type", '') != "snapshot"] @@ -54,12 +61,15 @@ class IndexView(tables.MultiTableView): return images def get_snapshots_data(self): + req = self.request + marker = req.GET.get(SnapshotsTable._meta.pagination_param, None) try: - snapshots = api.snapshot_list_detailed(self.request) + snaps, self._more_snapshots = api.snapshot_list_detailed(req, + marker=marker) except: - snapshots = [] - exceptions.handle(self.request, _("Unable to retrieve snapshots.")) - return snapshots + snaps = [] + exceptions.handle(req, _("Unable to retrieve snapshots.")) + return snaps def get_volume_snapshots_data(self): try: diff --git a/horizon/dashboards/nova/instances_and_volumes/instances/tests.py b/horizon/dashboards/nova/instances_and_volumes/instances/tests.py index 11da77067e..527df6be66 100644 --- a/horizon/dashboards/nova/instances_and_volumes/instances/tests.py +++ b/horizon/dashboards/nova/instances_and_volumes/instances/tests.py @@ -287,8 +287,10 @@ class InstanceViewTests(test.TestCase): server.id, "snapshot1") api.server_get(IsA(http.HttpRequest), server.id).AndReturn(server) - api.snapshot_list_detailed(IsA(http.HttpRequest)).AndReturn([]) - api.image_list_detailed(IsA(http.HttpRequest)).AndReturn([]) + api.snapshot_list_detailed(IsA(http.HttpRequest), marker=None) \ + .AndReturn(([], False)) + api.image_list_detailed(IsA(http.HttpRequest), marker=None) \ + .AndReturn(([], False)) api.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([]) api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list()) diff --git a/horizon/dashboards/syspanel/images/views.py b/horizon/dashboards/syspanel/images/views.py index d4821c52c2..c2b9775fbd 100644 --- a/horizon/dashboards/syspanel/images/views.py +++ b/horizon/dashboards/syspanel/images/views.py @@ -37,11 +37,18 @@ class IndexView(tables.DataTableView): table_class = AdminImagesTable template_name = 'syspanel/images/index.html' + def has_more_data(self, table): + return self._more + def get_data(self): images = [] + marker = self.request.GET.get(AdminImagesTable._meta.pagination_param, + None) try: - images = api.image_list_detailed(self.request) + images, self._more = api.image_list_detailed(self.request, + marker=marker) except: + self._more = False msg = _('Unable to retrieve image list.') exceptions.handle(self.request, msg) return images diff --git a/horizon/tables/base.py b/horizon/tables/base.py index 66b3355c52..bea4654b54 100644 --- a/horizon/tables/base.py +++ b/horizon/tables/base.py @@ -558,6 +558,13 @@ class DataTableOptions(object): The name of the context variable which will contain the table when it is rendered. Defaults to ``"table"``. + .. attribute:: pagination_param + + The name of the query string parameter which will be used when + paginating this table. When using multiple tables in a single + view this will need to be changed to differentiate between the + tables. Default: ``"marker"``. + .. attribute:: status_columns A list or tuple of column names which represents the "state" @@ -592,6 +599,7 @@ class DataTableOptions(object): self.row_actions = getattr(options, 'row_actions', []) self.row_class = getattr(options, 'row_class', Row) self.column_class = getattr(options, 'column_class', Column) + self.pagination_param = getattr(options, 'pagination_param', 'marker') # Set self.filter if we have any FilterActions filter_actions = [action for action in self.table_actions if @@ -1038,6 +1046,10 @@ class DataTable(object): """ return http.urlquote_plus(self.get_object_id(self.data[-1])) + def get_pagination_string(self): + """ Returns the query parameter string to paginate this table. """ + return "=".join([self._meta.pagination_param, self.get_marker()]) + def calculate_row_status(self, statuses): """ Returns a boolean value determining the overall row status diff --git a/horizon/templates/horizon/common/_data_table.html b/horizon/templates/horizon/common/_data_table.html index e64ebf70b6..54c743d872 100644 --- a/horizon/templates/horizon/common/_data_table.html +++ b/horizon/templates/horizon/common/_data_table.html @@ -31,7 +31,7 @@ {% blocktrans count counter=rows|length %}Displaying {{ counter }} item{% plural %}Displaying {{ counter }} items{% endblocktrans %} {% if table.has_more_data %} | - More » + More » {% endif %} diff --git a/horizon/tests/api_tests/glance_tests.py b/horizon/tests/api_tests/glance_tests.py index f6960971f4..f5f858ca6b 100644 --- a/horizon/tests/api_tests/glance_tests.py +++ b/horizon/tests/api_tests/glance_tests.py @@ -19,6 +19,7 @@ # under the License. from glance.common import exception as glance_exception +from django.conf import settings from horizon import api from horizon import test @@ -48,15 +49,14 @@ class GlanceApiTests(test.APITestCase): def test_image_list_detailed(self): """ Verify "list" returns our custom Image class. """ images = self.images.list() - glanceclient = self.stub_glanceclient() - glanceclient.get_images_detailed().AndReturn(images) + glanceclient.get_images_detailed(filters={}, limit=1001, marker=None) \ + .AndReturn([images, False]) self.mox.ReplayAll() - ret_val = api.image_list_detailed(self.request) + ret_val, _more = api.image_list_detailed(self.request) for image in ret_val: self.assertIsInstance(image, api.glance.Image) - self.assertIn(image._apidict, images) def test_glance_exception_wrapping_for_internal_server_errors(self): """ @@ -66,8 +66,8 @@ class GlanceApiTests(test.APITestCase): """ # TODO(johnp): Remove once Bug 952618 is fixed in the glance client. glanceclient = self.stub_glanceclient() - glanceclient.get_images_detailed().AndRaise( - Exception("Internal Server error: ")) + glanceclient.get_images_detailed(filters={}, limit=1001, marker=None) \ + .AndRaise(Exception("Internal Server error: ")) self.mox.ReplayAll() with self.assertRaises(glance_exception.ClientConnectionError): @@ -80,8 +80,9 @@ class GlanceApiTests(test.APITestCase): """ # TODO(johnp): Remove once Bug 952618 is fixed in the glance client. glanceclient = self.stub_glanceclient() - glanceclient.get_images_detailed().AndRaise( - Exception("Unknown error occurred! 503 Service Unavailable")) + exc = Exception("Unknown error occurred! 503 Service Unavailable") + glanceclient.get_images_detailed(filters={}, limit=1001, marker=None) \ + .AndRaise(exc) self.mox.ReplayAll() with self.assertRaises(glance_exception.ClientConnectionError):