Add "prev" link to instance page list pagination
Currently there is no link to previous page at paginated instances
table. This patch resolves that issue by re-using the pagination
code for flavors.
It also supports Ying Zuo's scenario:
After I set only 1 item per page and deleted the instance on the first page,
the expected behavior is showing the next instance in the table after one is
deleted.
xxxIndexView uses PagedTableMixin's _get_marker() from now instead of GET()-
ing the markers
Closes-Bug: #1274427
Co-Authored-By: Dmitry Ratushnyy <dratushn@cisco.com>
Co-Authored-By: Akihiro Motoki <amotoki@gmail.com>
Change-Id: Id8eaae6bf1b5d6f42291291655e14b8715c08bc8
Signed-off-by: Ferenc Cserepkei <ferenc.cserepkei@ericsson.com>
(cherry picked from commit 4676694179
)
This commit is contained in:
parent
7f218c8e7d
commit
44a098abcf
|
@ -372,8 +372,7 @@ def flavor_list(request, is_public=True, get_extras=False):
|
||||||
|
|
||||||
|
|
||||||
@profiler.trace
|
@profiler.trace
|
||||||
def update_pagination(entities, page_size, marker, sort_dir, sort_key,
|
def update_pagination(entities, page_size, marker, reversed_order=False):
|
||||||
reversed_order):
|
|
||||||
has_more_data = has_prev_data = False
|
has_more_data = has_prev_data = False
|
||||||
if len(entities) > page_size:
|
if len(entities) > page_size:
|
||||||
has_more_data = True
|
has_more_data = True
|
||||||
|
@ -389,9 +388,7 @@ def update_pagination(entities, page_size, marker, sort_dir, sort_key,
|
||||||
|
|
||||||
# restore the original ordering here
|
# restore the original ordering here
|
||||||
if reversed_order:
|
if reversed_order:
|
||||||
entities = sorted(entities, key=lambda entity:
|
entities.reverse()
|
||||||
(getattr(entity, sort_key) or '').lower(),
|
|
||||||
reverse=(sort_dir == 'asc'))
|
|
||||||
|
|
||||||
return entities, has_more_data, has_prev_data
|
return entities, has_more_data, has_prev_data
|
||||||
|
|
||||||
|
@ -415,7 +412,7 @@ def flavor_list_paged(request, is_public=True, get_extras=False, marker=None,
|
||||||
sort_key=sort_key,
|
sort_key=sort_key,
|
||||||
sort_dir=sort_dir)
|
sort_dir=sort_dir)
|
||||||
flavors, has_more_data, has_prev_data = update_pagination(
|
flavors, has_more_data, has_prev_data = update_pagination(
|
||||||
flavors, page_size, marker, sort_dir, sort_key, reversed_order)
|
flavors, page_size, marker, reversed_order)
|
||||||
else:
|
else:
|
||||||
flavors = novaclient(request).flavors.list(is_public=is_public)
|
flavors = novaclient(request).flavors.list(is_public=is_public)
|
||||||
|
|
||||||
|
@ -546,6 +543,14 @@ def server_create(request, name, image, flavor, key_name, user_data,
|
||||||
@profiler.trace
|
@profiler.trace
|
||||||
def server_delete(request, instance_id):
|
def server_delete(request, instance_id):
|
||||||
novaclient(request).servers.delete(instance_id)
|
novaclient(request).servers.delete(instance_id)
|
||||||
|
# Session is available and consistent for the current view
|
||||||
|
# among Horizon django servers even in load-balancing setup,
|
||||||
|
# so only the view listing the servers will recognize it as
|
||||||
|
# own DeleteInstance action performed. Note that dict is passed
|
||||||
|
# by reference in python. Quote from django's developer manual:
|
||||||
|
# " You can read it and write to request.session at any point
|
||||||
|
# in your view. You can edit it multiple times."
|
||||||
|
request.session['server_deleted'] = instance_id
|
||||||
|
|
||||||
|
|
||||||
def get_novaclient_with_locked_status(request):
|
def get_novaclient_with_locked_status(request):
|
||||||
|
@ -565,33 +570,66 @@ def server_get(request, instance_id):
|
||||||
|
|
||||||
|
|
||||||
@profiler.trace
|
@profiler.trace
|
||||||
def server_list(request, search_opts=None, detailed=True):
|
def server_list_paged(request,
|
||||||
|
search_opts=None,
|
||||||
|
detailed=True,
|
||||||
|
sort_dir="desc"):
|
||||||
|
has_more_data = False
|
||||||
|
has_prev_data = False
|
||||||
nova_client = get_novaclient_with_locked_status(request)
|
nova_client = get_novaclient_with_locked_status(request)
|
||||||
page_size = utils.get_page_size(request)
|
page_size = utils.get_page_size(request)
|
||||||
paginate = False
|
search_opts = {} if search_opts is None else search_opts
|
||||||
if search_opts is None:
|
marker = search_opts.get('marker', None)
|
||||||
search_opts = {}
|
|
||||||
elif 'paginate' in search_opts:
|
|
||||||
paginate = search_opts.pop('paginate')
|
|
||||||
if paginate:
|
|
||||||
search_opts['limit'] = page_size + 1
|
|
||||||
|
|
||||||
all_tenants = search_opts.get('all_tenants', False)
|
if not search_opts.get('all_tenants', False):
|
||||||
if all_tenants:
|
|
||||||
search_opts['all_tenants'] = True
|
|
||||||
else:
|
|
||||||
search_opts['project_id'] = request.user.tenant_id
|
search_opts['project_id'] = request.user.tenant_id
|
||||||
|
|
||||||
servers = [Server(s, request)
|
if search_opts.pop('paginate', False):
|
||||||
for s in nova_client.servers.list(detailed, search_opts)]
|
reversed_order = sort_dir is "asc"
|
||||||
|
LOG.debug("Notify received on deleted server: %r",
|
||||||
|
('server_deleted' in request.session))
|
||||||
|
deleted = request.session.pop('server_deleted',
|
||||||
|
None)
|
||||||
|
view_marker = 'possibly_deleted' if deleted and marker else 'ok'
|
||||||
|
search_opts['marker'] = deleted if deleted else marker
|
||||||
|
search_opts['limit'] = page_size + 1
|
||||||
|
search_opts['sort_dir'] = sort_dir
|
||||||
|
servers = [Server(s, request)
|
||||||
|
for s in nova_client.servers.list(detailed, search_opts)]
|
||||||
|
if view_marker == 'possibly_deleted':
|
||||||
|
if len(servers) == 0:
|
||||||
|
view_marker = 'head_deleted'
|
||||||
|
search_opts['sort_dir'] = 'desc'
|
||||||
|
reversed_order = False
|
||||||
|
servers = [Server(s, request)
|
||||||
|
for s in nova_client.servers.list(detailed,
|
||||||
|
search_opts)]
|
||||||
|
if len(servers) == 0:
|
||||||
|
view_marker = 'tail_deleted'
|
||||||
|
search_opts['sort_dir'] = 'asc'
|
||||||
|
reversed_order = True
|
||||||
|
servers = [Server(s, request)
|
||||||
|
for s in nova_client.servers.list(detailed,
|
||||||
|
search_opts)]
|
||||||
|
(servers, has_more_data, has_prev_data) = update_pagination(
|
||||||
|
servers, page_size, marker, reversed_order)
|
||||||
|
has_prev_data = (False
|
||||||
|
if view_marker == 'head_deleted'
|
||||||
|
else has_prev_data)
|
||||||
|
has_more_data = (False
|
||||||
|
if view_marker == 'tail_deleted'
|
||||||
|
else has_more_data)
|
||||||
|
else:
|
||||||
|
servers = [Server(s, request)
|
||||||
|
for s in nova_client.servers.list(detailed, search_opts)]
|
||||||
|
return (servers, has_more_data, has_prev_data)
|
||||||
|
|
||||||
has_more_data = False
|
|
||||||
if paginate and len(servers) > page_size:
|
@profiler.trace
|
||||||
servers.pop(-1)
|
def server_list(request, search_opts=None, detailed=True):
|
||||||
has_more_data = True
|
(servers, has_more_data, _) = server_list_paged(request,
|
||||||
elif paginate and len(servers) == getattr(settings, 'API_RESULT_LIMIT',
|
search_opts,
|
||||||
1000):
|
detailed)
|
||||||
has_more_data = True
|
|
||||||
return (servers, has_more_data)
|
return (servers, has_more_data)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -13,10 +13,13 @@
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.test import override_settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from openstack_dashboard import api
|
from openstack_dashboard import api
|
||||||
|
@ -29,7 +32,7 @@ INDEX_TEMPLATE = 'horizon/common/_data_table_view.html'
|
||||||
|
|
||||||
class InstanceViewTest(test.BaseAdminViewTests):
|
class InstanceViewTest(test.BaseAdminViewTests):
|
||||||
@test.create_mocks({
|
@test.create_mocks({
|
||||||
api.nova: ['flavor_list', 'server_list', 'extension_supported'],
|
api.nova: ['flavor_list', 'server_list_paged', 'extension_supported'],
|
||||||
api.keystone: ['tenant_list'],
|
api.keystone: ['tenant_list'],
|
||||||
api.glance: ['image_list_detailed_by_ids'],
|
api.glance: ['image_list_detailed_by_ids'],
|
||||||
})
|
})
|
||||||
|
@ -42,7 +45,7 @@ class InstanceViewTest(test.BaseAdminViewTests):
|
||||||
self.mock_tenant_list.return_value = [self.tenants.list(), False]
|
self.mock_tenant_list.return_value = [self.tenants.list(), False]
|
||||||
self.mock_image_list_detailed_by_ids.return_value = self.images.list()
|
self.mock_image_list_detailed_by_ids.return_value = self.images.list()
|
||||||
self.mock_flavor_list.return_value = self.flavors.list()
|
self.mock_flavor_list.return_value = self.flavors.list()
|
||||||
self.mock_server_list.return_value = [servers, False]
|
self.mock_server_list_paged.return_value = [servers, False, False]
|
||||||
|
|
||||||
res = self.client.get(INDEX_URL)
|
res = self.client.get(INDEX_URL)
|
||||||
self.assertTemplateUsed(res, INDEX_TEMPLATE)
|
self.assertTemplateUsed(res, INDEX_TEMPLATE)
|
||||||
|
@ -59,11 +62,13 @@ class InstanceViewTest(test.BaseAdminViewTests):
|
||||||
test.IsHttpRequest(), instances_img_ids)
|
test.IsHttpRequest(), instances_img_ids)
|
||||||
self.mock_flavor_list.assert_called_once_with(test.IsHttpRequest())
|
self.mock_flavor_list.assert_called_once_with(test.IsHttpRequest())
|
||||||
search_opts = {'marker': None, 'paginate': True, 'all_tenants': True}
|
search_opts = {'marker': None, 'paginate': True, 'all_tenants': True}
|
||||||
self.mock_server_list.assert_called_once_with(
|
self.mock_server_list_paged.assert_called_once_with(
|
||||||
test.IsHttpRequest(), search_opts=search_opts)
|
test.IsHttpRequest(),
|
||||||
|
sort_dir='desc',
|
||||||
|
search_opts=search_opts)
|
||||||
|
|
||||||
@test.create_mocks({
|
@test.create_mocks({
|
||||||
api.nova: ['flavor_list', 'flavor_get', 'server_list',
|
api.nova: ['flavor_list', 'flavor_get', 'server_list_paged',
|
||||||
'extension_supported'],
|
'extension_supported'],
|
||||||
api.keystone: ['tenant_list'],
|
api.keystone: ['tenant_list'],
|
||||||
api.glance: ['image_list_detailed_by_ids'],
|
api.glance: ['image_list_detailed_by_ids'],
|
||||||
|
@ -74,7 +79,7 @@ class InstanceViewTest(test.BaseAdminViewTests):
|
||||||
instances_img_ids = [instance.image.get('id') for instance in
|
instances_img_ids = [instance.image.get('id') for instance in
|
||||||
servers if hasattr(instance, 'image')]
|
servers if hasattr(instance, 'image')]
|
||||||
full_flavors = OrderedDict([(f.id, f) for f in flavors])
|
full_flavors = OrderedDict([(f.id, f) for f in flavors])
|
||||||
self.mock_server_list.return_value = [servers, False]
|
self.mock_server_list_paged.return_value = [servers, False, False]
|
||||||
self.mock_extension_supported.return_value = True
|
self.mock_extension_supported.return_value = True
|
||||||
self.mock_flavor_list.side_effect = self.exceptions.nova
|
self.mock_flavor_list.side_effect = self.exceptions.nova
|
||||||
self.mock_tenant_list.return_value = [self.tenants.list(), False]
|
self.mock_tenant_list.return_value = [self.tenants.list(), False]
|
||||||
|
@ -92,8 +97,10 @@ class InstanceViewTest(test.BaseAdminViewTests):
|
||||||
self.assertItemsEqual(instances, servers)
|
self.assertItemsEqual(instances, servers)
|
||||||
|
|
||||||
search_opts = {'marker': None, 'paginate': True, 'all_tenants': True}
|
search_opts = {'marker': None, 'paginate': True, 'all_tenants': True}
|
||||||
self.mock_server_list.assert_called_once_with(
|
self.mock_server_list_paged.assert_called_once_with(
|
||||||
test.IsHttpRequest(), search_opts=search_opts)
|
test.IsHttpRequest(),
|
||||||
|
sort_dir='desc',
|
||||||
|
search_opts=search_opts)
|
||||||
self.mock_extension_supported.assert_has_calls([
|
self.mock_extension_supported.assert_has_calls([
|
||||||
mock.call('AdminActions', test.IsHttpRequest()),
|
mock.call('AdminActions', test.IsHttpRequest()),
|
||||||
mock.call('AdminActions', test.IsHttpRequest()),
|
mock.call('AdminActions', test.IsHttpRequest()),
|
||||||
|
@ -108,7 +115,7 @@ class InstanceViewTest(test.BaseAdminViewTests):
|
||||||
test.IsHttpRequest(), instances_img_ids)
|
test.IsHttpRequest(), instances_img_ids)
|
||||||
|
|
||||||
@test.create_mocks({
|
@test.create_mocks({
|
||||||
api.nova: ['flavor_list', 'flavor_get', 'server_list',
|
api.nova: ['flavor_list', 'flavor_get', 'server_list_paged',
|
||||||
'extension_supported'],
|
'extension_supported'],
|
||||||
api.keystone: ['tenant_list'],
|
api.keystone: ['tenant_list'],
|
||||||
api.glance: ['image_list_detailed_by_ids'],
|
api.glance: ['image_list_detailed_by_ids'],
|
||||||
|
@ -124,7 +131,7 @@ class InstanceViewTest(test.BaseAdminViewTests):
|
||||||
|
|
||||||
self.mock_image_list_detailed_by_ids.return_value = self.images.list()
|
self.mock_image_list_detailed_by_ids.return_value = self.images.list()
|
||||||
self.mock_flavor_list.return_value = self.flavors.list()
|
self.mock_flavor_list.return_value = self.flavors.list()
|
||||||
self.mock_server_list.return_value = [servers, False]
|
self.mock_server_list_paged.return_value = [servers, False, False]
|
||||||
self.mock_extension_supported.return_value = True
|
self.mock_extension_supported.return_value = True
|
||||||
self.mock_tenant_list.return_value = [self.tenants.list(), False]
|
self.mock_tenant_list.return_value = [self.tenants.list(), False]
|
||||||
self.mock_flavor_get.side_effect = self.exceptions.nova
|
self.mock_flavor_get.side_effect = self.exceptions.nova
|
||||||
|
@ -142,8 +149,10 @@ class InstanceViewTest(test.BaseAdminViewTests):
|
||||||
test.IsHttpRequest(), instances_img_ids)
|
test.IsHttpRequest(), instances_img_ids)
|
||||||
self.mock_flavor_list.assert_called_once_with(test.IsHttpRequest())
|
self.mock_flavor_list.assert_called_once_with(test.IsHttpRequest())
|
||||||
search_opts = {'marker': None, 'paginate': True, 'all_tenants': True}
|
search_opts = {'marker': None, 'paginate': True, 'all_tenants': True}
|
||||||
self.mock_server_list.assert_called_once_with(
|
self.mock_server_list_paged.assert_called_once_with(
|
||||||
test.IsHttpRequest(), search_opts=search_opts)
|
test.IsHttpRequest(),
|
||||||
|
sort_dir='desc',
|
||||||
|
search_opts=search_opts)
|
||||||
self.mock_extension_supported.assert_has_calls([
|
self.mock_extension_supported.assert_has_calls([
|
||||||
mock.call('AdminActions', test.IsHttpRequest()),
|
mock.call('AdminActions', test.IsHttpRequest()),
|
||||||
mock.call('AdminActions', test.IsHttpRequest()),
|
mock.call('AdminActions', test.IsHttpRequest()),
|
||||||
|
@ -155,12 +164,12 @@ class InstanceViewTest(test.BaseAdminViewTests):
|
||||||
self.assertEqual(len(servers), self.mock_flavor_get.call_count)
|
self.assertEqual(len(servers), self.mock_flavor_get.call_count)
|
||||||
|
|
||||||
@test.create_mocks({
|
@test.create_mocks({
|
||||||
api.nova: ['server_list', 'flavor_list'],
|
api.nova: ['server_list_paged', 'flavor_list'],
|
||||||
api.keystone: ['tenant_list'],
|
api.keystone: ['tenant_list'],
|
||||||
api.glance: ['image_list_detailed_by_ids'],
|
api.glance: ['image_list_detailed_by_ids'],
|
||||||
})
|
})
|
||||||
def test_index_server_list_exception(self):
|
def test_index_server_list_exception(self):
|
||||||
self.mock_server_list.side_effect = self.exceptions.nova
|
self.mock_server_list_paged.side_effect = self.exceptions.nova
|
||||||
self.mock_flavor_list.return_value = self.flavors.list()
|
self.mock_flavor_list.return_value = self.flavors.list()
|
||||||
self.mock_tenant_list.return_value = [self.tenants.list(), False]
|
self.mock_tenant_list.return_value = [self.tenants.list(), False]
|
||||||
self.mock_image_list_detailed_by_ids.return_value = self.images.list()
|
self.mock_image_list_detailed_by_ids.return_value = self.images.list()
|
||||||
|
@ -170,8 +179,9 @@ class InstanceViewTest(test.BaseAdminViewTests):
|
||||||
self.assertEqual(len(res.context['instances_table'].data), 0)
|
self.assertEqual(len(res.context['instances_table'].data), 0)
|
||||||
|
|
||||||
search_opts = {'marker': None, 'paginate': True, 'all_tenants': True}
|
search_opts = {'marker': None, 'paginate': True, 'all_tenants': True}
|
||||||
self.mock_server_list.assert_called_once_with(
|
self.mock_server_list_paged.assert_called_once_with(
|
||||||
test.IsHttpRequest(),
|
test.IsHttpRequest(),
|
||||||
|
sort_dir='desc',
|
||||||
search_opts=search_opts)
|
search_opts=search_opts)
|
||||||
self.mock_tenant_list.assert_called_once_with(test.IsHttpRequest())
|
self.mock_tenant_list.assert_called_once_with(test.IsHttpRequest())
|
||||||
self.mock_image_list_detailed_by_ids.assert_called_once_with(
|
self.mock_image_list_detailed_by_ids.assert_called_once_with(
|
||||||
|
@ -223,7 +233,7 @@ class InstanceViewTest(test.BaseAdminViewTests):
|
||||||
test.IsHttpRequest(), [server])
|
test.IsHttpRequest(), [server])
|
||||||
|
|
||||||
@test.create_mocks({
|
@test.create_mocks({
|
||||||
api.nova: ['flavor_list', 'server_list', 'extension_supported'],
|
api.nova: ['flavor_list', 'server_list_paged', 'extension_supported'],
|
||||||
api.keystone: ['tenant_list'],
|
api.keystone: ['tenant_list'],
|
||||||
api.glance: ['image_list_detailed_by_ids'],
|
api.glance: ['image_list_detailed_by_ids'],
|
||||||
})
|
})
|
||||||
|
@ -234,9 +244,9 @@ class InstanceViewTest(test.BaseAdminViewTests):
|
||||||
self.mock_tenant_list.return_value = [self.tenants.list(), False]
|
self.mock_tenant_list.return_value = [self.tenants.list(), False]
|
||||||
self.mock_image_list_detailed_by_ids.return_value = self.images.list()
|
self.mock_image_list_detailed_by_ids.return_value = self.images.list()
|
||||||
self.mock_flavor_list.return_value = self.flavors.list()
|
self.mock_flavor_list.return_value = self.flavors.list()
|
||||||
self.mock_server_list.return_value = [self.servers.list(), False]
|
self.mock_server_list_paged.return_value = [
|
||||||
|
self.servers.list(), False, False]
|
||||||
self.mock_extension_supported.return_value = True
|
self.mock_extension_supported.return_value = True
|
||||||
|
|
||||||
res = self.client.get(INDEX_URL)
|
res = self.client.get(INDEX_URL)
|
||||||
self.assertContains(res, "instances__migrate")
|
self.assertContains(res, "instances__migrate")
|
||||||
self.assertNotContains(res, "instances__confirm")
|
self.assertNotContains(res, "instances__confirm")
|
||||||
|
@ -247,8 +257,10 @@ class InstanceViewTest(test.BaseAdminViewTests):
|
||||||
test.IsHttpRequest(), instances_img_ids)
|
test.IsHttpRequest(), instances_img_ids)
|
||||||
self.mock_flavor_list.assert_called_once_with(test.IsHttpRequest())
|
self.mock_flavor_list.assert_called_once_with(test.IsHttpRequest())
|
||||||
search_opts = {'marker': None, 'paginate': True, 'all_tenants': True}
|
search_opts = {'marker': None, 'paginate': True, 'all_tenants': True}
|
||||||
self.mock_server_list.assert_called_once_with(
|
self.mock_server_list_paged.assert_called_once_with(
|
||||||
test.IsHttpRequest(), search_opts=search_opts)
|
test.IsHttpRequest(),
|
||||||
|
sort_dir='desc',
|
||||||
|
search_opts=search_opts)
|
||||||
self.mock_extension_supported.assert_has_calls([
|
self.mock_extension_supported.assert_has_calls([
|
||||||
mock.call('AdminActions', test.IsHttpRequest()),
|
mock.call('AdminActions', test.IsHttpRequest()),
|
||||||
mock.call('AdminActions', test.IsHttpRequest()),
|
mock.call('AdminActions', test.IsHttpRequest()),
|
||||||
|
@ -256,7 +268,7 @@ class InstanceViewTest(test.BaseAdminViewTests):
|
||||||
self.assertEqual(12, self.mock_extension_supported.call_count)
|
self.assertEqual(12, self.mock_extension_supported.call_count)
|
||||||
|
|
||||||
@test.create_mocks({
|
@test.create_mocks({
|
||||||
api.nova: ['flavor_list', 'server_list', 'extension_supported'],
|
api.nova: ['flavor_list', 'server_list_paged', 'extension_supported'],
|
||||||
api.keystone: ['tenant_list'],
|
api.keystone: ['tenant_list'],
|
||||||
api.glance: ['image_list_detailed_by_ids'],
|
api.glance: ['image_list_detailed_by_ids'],
|
||||||
})
|
})
|
||||||
|
@ -272,7 +284,7 @@ class InstanceViewTest(test.BaseAdminViewTests):
|
||||||
self.mock_image_list_detailed_by_ids.return_value = self.images.list()
|
self.mock_image_list_detailed_by_ids.return_value = self.images.list()
|
||||||
self.mock_flavor_list.return_value = self.flavors.list()
|
self.mock_flavor_list.return_value = self.flavors.list()
|
||||||
self.mock_extension_supported.return_value = True
|
self.mock_extension_supported.return_value = True
|
||||||
self.mock_server_list.return_value = [servers, False]
|
self.mock_server_list_paged.return_value = [servers, False, False]
|
||||||
|
|
||||||
res = self.client.get(INDEX_URL)
|
res = self.client.get(INDEX_URL)
|
||||||
self.assertContains(res, "instances__confirm")
|
self.assertContains(res, "instances__confirm")
|
||||||
|
@ -289,8 +301,10 @@ class InstanceViewTest(test.BaseAdminViewTests):
|
||||||
mock.call('Shelve', test.IsHttpRequest())] * 4)
|
mock.call('Shelve', test.IsHttpRequest())] * 4)
|
||||||
self.assertEqual(12, self.mock_extension_supported.call_count)
|
self.assertEqual(12, self.mock_extension_supported.call_count)
|
||||||
search_opts = {'marker': None, 'paginate': True, 'all_tenants': True}
|
search_opts = {'marker': None, 'paginate': True, 'all_tenants': True}
|
||||||
self.mock_server_list.assert_called_once_with(
|
self.mock_server_list_paged.assert_called_once_with(
|
||||||
test.IsHttpRequest(), search_opts=search_opts)
|
test.IsHttpRequest(),
|
||||||
|
sort_dir='desc',
|
||||||
|
search_opts=search_opts)
|
||||||
|
|
||||||
@test.create_mocks({api.nova: ['service_list',
|
@test.create_mocks({api.nova: ['service_list',
|
||||||
'server_get']})
|
'server_get']})
|
||||||
|
@ -474,3 +488,125 @@ class InstanceViewTest(test.BaseAdminViewTests):
|
||||||
self.assertTemplateUsed(res, INDEX_TEMPLATE)
|
self.assertTemplateUsed(res, INDEX_TEMPLATE)
|
||||||
instances = res.context['table'].data
|
instances = res.context['table'].data
|
||||||
self.assertItemsEqual(instances, [])
|
self.assertItemsEqual(instances, [])
|
||||||
|
|
||||||
|
@test.create_mocks({
|
||||||
|
api.nova: ['flavor_list',
|
||||||
|
'flavor_get',
|
||||||
|
'server_list_paged',
|
||||||
|
'extension_supported'],
|
||||||
|
api.keystone: ['tenant_list'],
|
||||||
|
api.glance: ['image_list_detailed_by_ids'],
|
||||||
|
})
|
||||||
|
def _test_servers_paginate_do(self,
|
||||||
|
marker,
|
||||||
|
servers,
|
||||||
|
has_more,
|
||||||
|
has_prev):
|
||||||
|
flavors = self.flavors.list()
|
||||||
|
tenants = self.tenants.list()
|
||||||
|
images = self.images.list()
|
||||||
|
# UUID indices are unique and are guaranteed being deterministic.
|
||||||
|
for i, server in enumerate(servers):
|
||||||
|
server.flavor['id'] = str(uuid.UUID(int=i))
|
||||||
|
|
||||||
|
self.mock_server_list_paged.return_value = [
|
||||||
|
servers, has_more, has_prev]
|
||||||
|
self.mock_extension_supported.return_value = True
|
||||||
|
self.mock_flavor_list.return_value = flavors
|
||||||
|
self.mock_image_list_detailed_by_ids.return_value = images
|
||||||
|
self.mock_tenant_list.return_value = [tenants, False]
|
||||||
|
self.mock_flavor_get.side_effect = self.exceptions.nova
|
||||||
|
|
||||||
|
if marker:
|
||||||
|
url = "?".join([INDEX_URL, "marker={}".format(marker)])
|
||||||
|
else:
|
||||||
|
url = INDEX_URL
|
||||||
|
res = self.client.get(url)
|
||||||
|
self.assertTemplateUsed(res, INDEX_TEMPLATE)
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
|
||||||
|
self.mock_extension_supported.assert_has_calls([
|
||||||
|
mock.call('AdminActions', test.IsHttpRequest()),
|
||||||
|
mock.call('AdminActions', test.IsHttpRequest()),
|
||||||
|
mock.call('Shelve', test.IsHttpRequest())])
|
||||||
|
self.assertEqual(3, self.mock_extension_supported.call_count)
|
||||||
|
self.mock_tenant_list.assert_called_once_with(test.IsHttpRequest())
|
||||||
|
self.mock_image_list_detailed_by_ids.assert_called_once_with(
|
||||||
|
test.IsHttpRequest(),
|
||||||
|
[server.image.id for server in servers])
|
||||||
|
self.mock_flavor_list.assert_called_once_with(test.IsHttpRequest())
|
||||||
|
search_opts = {'marker': marker, 'paginate': True, 'all_tenants': True}
|
||||||
|
self.mock_server_list_paged.assert_called_once_with(
|
||||||
|
test.IsHttpRequest(),
|
||||||
|
sort_dir='desc',
|
||||||
|
search_opts=search_opts)
|
||||||
|
self.mock_flavor_get.assert_has_calls(
|
||||||
|
[mock.call(test.IsHttpRequest(), s.flavor['id']) for s in servers])
|
||||||
|
self.assertEqual(len(servers), self.mock_flavor_get.call_count)
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
@override_settings(API_RESULT_PAGE_SIZE=1)
|
||||||
|
def test_severs_index_paginated(self):
|
||||||
|
size = settings.API_RESULT_PAGE_SIZE
|
||||||
|
mox_servers = self.servers.list()
|
||||||
|
|
||||||
|
# get first page
|
||||||
|
expected_servers = mox_servers[:size]
|
||||||
|
res = self._test_servers_paginate_do(
|
||||||
|
marker=None,
|
||||||
|
servers=expected_servers,
|
||||||
|
has_more=True,
|
||||||
|
has_prev=False)
|
||||||
|
servers = res.context['table'].data
|
||||||
|
self.assertItemsEqual(servers, expected_servers)
|
||||||
|
|
||||||
|
# get second page
|
||||||
|
expected_servers = mox_servers[size:2 * size]
|
||||||
|
marker = expected_servers[0].id
|
||||||
|
res = self._test_servers_paginate_do(
|
||||||
|
marker=marker,
|
||||||
|
servers=expected_servers,
|
||||||
|
has_more=True,
|
||||||
|
has_prev=True)
|
||||||
|
servers = res.context['table'].data
|
||||||
|
self.assertItemsEqual(servers, expected_servers)
|
||||||
|
|
||||||
|
# get last page
|
||||||
|
expected_servers = mox_servers[-size:]
|
||||||
|
marker = expected_servers[0].id
|
||||||
|
res = self._test_servers_paginate_do(
|
||||||
|
marker=marker,
|
||||||
|
servers=expected_servers,
|
||||||
|
has_more=False,
|
||||||
|
has_prev=True)
|
||||||
|
servers = res.context['table'].data
|
||||||
|
self.assertItemsEqual(servers, expected_servers)
|
||||||
|
|
||||||
|
@override_settings(API_RESULT_PAGE_SIZE=1)
|
||||||
|
def test_servers_index_paginated_prev(self):
|
||||||
|
size = settings.API_RESULT_PAGE_SIZE
|
||||||
|
mox_servers = self.servers.list()
|
||||||
|
|
||||||
|
# prev from some page
|
||||||
|
expected_servers = mox_servers[size:2 * size]
|
||||||
|
marker = mox_servers[0].id
|
||||||
|
|
||||||
|
res = self._test_servers_paginate_do(
|
||||||
|
marker=marker,
|
||||||
|
servers=expected_servers,
|
||||||
|
has_more=False,
|
||||||
|
has_prev=True)
|
||||||
|
servers = res.context['table'].data
|
||||||
|
self.assertItemsEqual(servers, expected_servers)
|
||||||
|
|
||||||
|
# back to first page
|
||||||
|
expected_servers = mox_servers[:size]
|
||||||
|
marker = mox_servers[0].id
|
||||||
|
res = self._test_servers_paginate_do(
|
||||||
|
marker=marker,
|
||||||
|
servers=expected_servers,
|
||||||
|
has_more=True,
|
||||||
|
has_prev=False)
|
||||||
|
servers = res.context['table'].data
|
||||||
|
self.assertItemsEqual(servers, expected_servers)
|
||||||
|
|
|
@ -70,10 +70,13 @@ class AdminUpdateView(views.UpdateView):
|
||||||
success_url = reverse_lazy("horizon:admin:instances:index")
|
success_url = reverse_lazy("horizon:admin:instances:index")
|
||||||
|
|
||||||
|
|
||||||
class AdminIndexView(tables.DataTableView):
|
class AdminIndexView(tables.PagedTableMixin, tables.DataTableView):
|
||||||
table_class = project_tables.AdminInstancesTable
|
table_class = project_tables.AdminInstancesTable
|
||||||
page_title = _("Instances")
|
page_title = _("Instances")
|
||||||
|
|
||||||
|
def has_prev_data(self, table):
|
||||||
|
return getattr(self, "_prev", False)
|
||||||
|
|
||||||
def has_more_data(self, table):
|
def has_more_data(self, table):
|
||||||
return self._more
|
return self._more
|
||||||
|
|
||||||
|
@ -115,21 +118,21 @@ class AdminIndexView(tables.DataTableView):
|
||||||
exceptions.handle(self.request, msg)
|
exceptions.handle(self.request, msg)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def _get_instances(self, search_opts):
|
def _get_instances(self, search_opts, sort_dir):
|
||||||
try:
|
try:
|
||||||
instances, self._more = api.nova.server_list(
|
instances, self._more, self._prev = api.nova.server_list_paged(
|
||||||
self.request,
|
self.request,
|
||||||
search_opts=search_opts)
|
search_opts=search_opts,
|
||||||
|
sort_dir=sort_dir)
|
||||||
except Exception:
|
except Exception:
|
||||||
self._more = False
|
self._more = self._prev = False
|
||||||
instances = []
|
instances = []
|
||||||
exceptions.handle(self.request,
|
exceptions.handle(self.request,
|
||||||
_('Unable to retrieve instance list.'))
|
_('Unable to retrieve instance list.'))
|
||||||
return instances
|
return instances
|
||||||
|
|
||||||
def get_data(self):
|
def get_data(self):
|
||||||
marker = self.request.GET.get(
|
marker, sort_dir = self._get_marker()
|
||||||
project_tables.AdminInstancesTable._meta.pagination_param, None)
|
|
||||||
default_search_opts = {'marker': marker,
|
default_search_opts = {'marker': marker,
|
||||||
'paginate': True,
|
'paginate': True,
|
||||||
'all_tenants': True}
|
'all_tenants': True}
|
||||||
|
@ -148,7 +151,7 @@ class AdminIndexView(tables.DataTableView):
|
||||||
|
|
||||||
self._needs_filter_first = False
|
self._needs_filter_first = False
|
||||||
|
|
||||||
instances = self._get_instances(search_opts)
|
instances = self._get_instances(search_opts, sort_dir)
|
||||||
results = futurist_utils.call_functions_parallel(
|
results = futurist_utils.call_functions_parallel(
|
||||||
(self._get_images, [tuple(instances)]),
|
(self._get_images, [tuple(instances)]),
|
||||||
self._get_flavors,
|
self._get_flavors,
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -59,10 +59,13 @@ from openstack_dashboard.views import get_url_with_pagination
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class IndexView(tables.DataTableView):
|
class IndexView(tables.PagedTableMixin, tables.DataTableView):
|
||||||
table_class = project_tables.InstancesTable
|
table_class = project_tables.InstancesTable
|
||||||
page_title = _("Instances")
|
page_title = _("Instances")
|
||||||
|
|
||||||
|
def has_prev_data(self, table):
|
||||||
|
return getattr(self, "_prev", False)
|
||||||
|
|
||||||
def has_more_data(self, table):
|
def has_more_data(self, table):
|
||||||
return self._more
|
return self._more
|
||||||
|
|
||||||
|
@ -85,13 +88,14 @@ class IndexView(tables.DataTableView):
|
||||||
exceptions.handle(self.request, ignore=True)
|
exceptions.handle(self.request, ignore=True)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def _get_instances(self, search_opts):
|
def _get_instances(self, search_opts, sort_dir):
|
||||||
try:
|
try:
|
||||||
instances, self._more = api.nova.server_list(
|
instances, self._more, self._prev = api.nova.server_list_paged(
|
||||||
self.request,
|
self.request,
|
||||||
search_opts=search_opts)
|
search_opts=search_opts,
|
||||||
|
sort_dir=sort_dir)
|
||||||
except Exception:
|
except Exception:
|
||||||
self._more = False
|
self._more = self._prev = False
|
||||||
instances = []
|
instances = []
|
||||||
exceptions.handle(self.request,
|
exceptions.handle(self.request,
|
||||||
_('Unable to retrieve instances.'))
|
_('Unable to retrieve instances.'))
|
||||||
|
@ -122,8 +126,7 @@ class IndexView(tables.DataTableView):
|
||||||
return instances
|
return instances
|
||||||
|
|
||||||
def get_data(self):
|
def get_data(self):
|
||||||
marker = self.request.GET.get(
|
marker, sort_dir = self._get_marker()
|
||||||
project_tables.InstancesTable._meta.pagination_param, None)
|
|
||||||
search_opts = self.get_filters({'marker': marker, 'paginate': True})
|
search_opts = self.get_filters({'marker': marker, 'paginate': True})
|
||||||
|
|
||||||
image_dict, flavor_dict = futurist_utils.call_functions_parallel(
|
image_dict, flavor_dict = futurist_utils.call_functions_parallel(
|
||||||
|
@ -137,7 +140,7 @@ class IndexView(tables.DataTableView):
|
||||||
self._more = False
|
self._more = False
|
||||||
return []
|
return []
|
||||||
|
|
||||||
instances = self._get_instances(search_opts)
|
instances = self._get_instances(search_opts, sort_dir)
|
||||||
|
|
||||||
# Loop through instances to get flavor info.
|
# Loop through instances to get flavor info.
|
||||||
for instance in instances:
|
for instance in instances:
|
||||||
|
|
|
@ -73,7 +73,7 @@ class TestInstances(helpers.TestCase):
|
||||||
first_page_definition = {'Next': True, 'Prev': False,
|
first_page_definition = {'Next': True, 'Prev': False,
|
||||||
'Count': items_per_page,
|
'Count': items_per_page,
|
||||||
'Names': [instance_list[1]]}
|
'Names': [instance_list[1]]}
|
||||||
second_page_definition = {'Next': False, 'Prev': False,
|
second_page_definition = {'Next': False, 'Prev': True,
|
||||||
'Count': items_per_page,
|
'Count': items_per_page,
|
||||||
'Names': [instance_list[0]]}
|
'Names': [instance_list[0]]}
|
||||||
settings_page = self.home_pg.go_to_settings_usersettingspage()
|
settings_page = self.home_pg.go_to_settings_usersettingspage()
|
||||||
|
|
|
@ -204,7 +204,8 @@ class ComputeApiTests(test.APIMockTestCase):
|
||||||
True,
|
True,
|
||||||
{'all_tenants': True,
|
{'all_tenants': True,
|
||||||
'marker': None,
|
'marker': None,
|
||||||
'limit': page_size + 1})
|
'limit': page_size + 1,
|
||||||
|
'sort_dir': 'desc'})
|
||||||
|
|
||||||
@override_settings(API_RESULT_PAGE_SIZE=1)
|
@override_settings(API_RESULT_PAGE_SIZE=1)
|
||||||
@mock.patch.object(api.nova, 'novaclient')
|
@mock.patch.object(api.nova, 'novaclient')
|
||||||
|
@ -229,7 +230,8 @@ class ComputeApiTests(test.APIMockTestCase):
|
||||||
True,
|
True,
|
||||||
{'all_tenants': True,
|
{'all_tenants': True,
|
||||||
'marker': None,
|
'marker': None,
|
||||||
'limit': page_size + 1})
|
'limit': page_size + 1,
|
||||||
|
'sort_dir': 'desc'})
|
||||||
|
|
||||||
@mock.patch.object(api.nova, 'novaclient')
|
@mock.patch.object(api.nova, 'novaclient')
|
||||||
def test_usage_get(self, mock_novaclient):
|
def test_usage_get(self, mock_novaclient):
|
||||||
|
|
Loading…
Reference in New Issue