diff --git a/horizon/tables/base.py b/horizon/tables/base.py index 1e78e4232d..955dcd313c 100644 --- a/horizon/tables/base.py +++ b/horizon/tables/base.py @@ -14,6 +14,7 @@ import collections import copy +import inspect import json import logging from operator import attrgetter @@ -466,6 +467,8 @@ class Column(html.HTMLElement): return None obj_id = self.table.get_object_id(datum) if callable(self.link): + if 'request' in inspect.getargspec(self.link).args: + return self.link(datum, request=self.table.request) return self.link(datum) try: return urlresolvers.reverse(self.link, args=(obj_id,)) @@ -670,11 +673,19 @@ class Row(html.HTMLElement): def get_ajax_update_url(self): table_url = self.table.get_absolute_url() - params = urlencode(collections.OrderedDict([ + marker_name = self.table._meta.pagination_param + marker = self.table.request.GET.get(marker_name, None) + if not marker: + marker_name = self.table._meta.prev_pagination_param + marker = self.table.request.GET.get(marker_name, None) + request_params = [ ("action", self.ajax_action_name), ("table", self.table.name), - ("obj_id", self.table.get_object_id(self.datum)) - ])) + ("obj_id", self.table.get_object_id(self.datum)), + ] + if marker: + request_params.append((marker_name, marker)) + params = urlencode(collections.OrderedDict(request_params)) return "%s?%s" % (table_url, params) def can_be_selected(self, datum): diff --git a/openstack_dashboard/dashboards/admin/instances/tables.py b/openstack_dashboard/dashboards/admin/instances/tables.py index ecdcba973b..15170928bf 100644 --- a/openstack_dashboard/dashboards/admin/instances/tables.py +++ b/openstack_dashboard/dashboards/admin/instances/tables.py @@ -26,6 +26,7 @@ from openstack_dashboard.dashboards.project.instances import audit_tables from openstack_dashboard.dashboards.project.instances \ import tables as project_tables from openstack_dashboard import policy +from openstack_dashboard.views import get_url_with_pagination class AdminEditInstance(project_tables.EditInstance): @@ -109,6 +110,13 @@ class AdminInstanceFilterAction(tables.FilterAction): ) + project_tables.INSTANCE_FILTER_CHOICES +def get_server_detail_link(obj, request): + return get_url_with_pagination( + request, AdminInstancesTable._meta.pagination_param, + AdminInstancesTable._meta.prev_pagination_param, + "horizon:admin:instances:detail", obj.id) + + class AdminInstancesTable(tables.DataTable): TASK_STATUS_CHOICES = ( (None, True), @@ -134,7 +142,7 @@ class AdminInstancesTable(tables.DataTable): verbose_name=_("Host"), classes=('nowrap-col',)) name = tables.WrappingColumn("name", - link="horizon:admin:instances:detail", + link=get_server_detail_link, verbose_name=_("Name")) image_name = tables.Column("image_name", verbose_name=_("Image Name")) diff --git a/openstack_dashboard/dashboards/project/instances/tables.py b/openstack_dashboard/dashboards/project/instances/tables.py index 2793a022b6..211b9ca8da 100644 --- a/openstack_dashboard/dashboards/project/instances/tables.py +++ b/openstack_dashboard/dashboards/project/instances/tables.py @@ -44,7 +44,7 @@ from openstack_dashboard.dashboards.project.instances.workflows \ from openstack_dashboard.dashboards.project.instances.workflows \ import update_instance from openstack_dashboard import policy - +from openstack_dashboard.views import get_url_with_pagination LOG = logging.getLogger(__name__) @@ -1207,6 +1207,14 @@ def render_locked(instance): return mark_safe(locked_status) +def get_server_detail_link(obj, request): + return get_url_with_pagination(request, + InstancesTable._meta.pagination_param, + InstancesTable._meta.prev_pagination_param, + 'horizon:project:instances:detail', + obj.id) + + class InstancesTable(tables.DataTable): TASK_STATUS_CHOICES = ( (None, True), @@ -1223,7 +1231,7 @@ class InstancesTable(tables.DataTable): ("shelved_offloaded", True), ) name = tables.WrappingColumn("name", - link="horizon:project:instances:detail", + link=get_server_detail_link, verbose_name=_("Instance Name")) image_name = tables.WrappingColumn("image_name", verbose_name=_("Image Name")) diff --git a/openstack_dashboard/dashboards/project/instances/tests.py b/openstack_dashboard/dashboards/project/instances/tests.py index 0f1e1cab3a..9b57ef6b1b 100644 --- a/openstack_dashboard/dashboards/project/instances/tests.py +++ b/openstack_dashboard/dashboards/project/instances/tests.py @@ -44,6 +44,7 @@ from openstack_dashboard.dashboards.project.instances import tabs from openstack_dashboard.dashboards.project.instances import workflows from openstack_dashboard.test import helpers from openstack_dashboard.usage import quotas +from openstack_dashboard.views import get_url_with_pagination INDEX_TEMPLATE = 'horizon/common/_data_table_view.html' @@ -611,12 +612,51 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): six.text_type(server.id)) self.mox.ReplayAll() - formData = {'action': 'instances__suspend__%s' % server.id} - res = self.client.post(INDEX_URL, formData) + url = get_url_with_pagination( + self.request, 'next', 'prev', 'horizon:project:instances:index') + res = self.client.post(url, formData) self.assertRedirectsNoFollow(res, INDEX_URL) + @django.test.utils.override_settings(API_RESULT_PAGE_SIZE=2) + @helpers.create_stubs({api.nova: ('server_suspend', + 'server_list', + 'flavor_list', + 'extension_supported', + 'is_feature_available',), + api.glance: ('image_list_detailed',), + api.network: ('servers_update_addresses',)}) + def test_suspend_instance_if_placed_on_2nd_page(self): + page_size = getattr(settings, 'API_RESULT_PAGE_SIZE', 2) + servers = self.servers.list()[:3] + + api.nova.extension_supported('AdminActions', IsA(http.HttpRequest)) \ + .MultipleTimes().AndReturn(True) + api.nova.flavor_list(IsA(http.HttpRequest)) \ + .AndReturn(self.flavors.list()) + api.glance.image_list_detailed(IgnoreArg()) \ + .AndReturn((self.images.list(), False, False)) + + api.nova.server_list(IsA(http.HttpRequest), search_opts={ + 'marker': servers[page_size - 1].id, 'paginate': True}) \ + .AndReturn([servers[page_size:], False]) + api.network.servers_update_addresses( + IsA(http.HttpRequest), servers[page_size:]) + api.nova.server_suspend(IsA(http.HttpRequest), + six.text_type(servers[-1].id)) + + self.mox.ReplayAll() + + self.request.GET['marker'] = servers[-2].id + params = "=".join([tables.InstancesTable._meta.pagination_param, + servers[page_size - 1].id]) + url = "?".join([reverse('horizon:project:instances:index'), + params]) + formData = {'action': 'instances__suspend__%s' % servers[-1].id} + + self.client.post(url, formData) + @helpers.create_stubs({api.nova: ('server_suspend', 'server_list', 'flavor_list', diff --git a/openstack_dashboard/dashboards/project/instances/views.py b/openstack_dashboard/dashboards/project/instances/views.py index a4fc9e561e..6a21f363c5 100644 --- a/openstack_dashboard/dashboards/project/instances/views.py +++ b/openstack_dashboard/dashboards/project/instances/views.py @@ -53,6 +53,7 @@ from openstack_dashboard.dashboards.project.instances \ import tabs as project_tabs from openstack_dashboard.dashboards.project.instances \ import workflows as project_workflows +from openstack_dashboard.views import get_url_with_pagination LOG = logging.getLogger(__name__) @@ -322,7 +323,12 @@ class DetailView(tabs.TabView): args=[instance.image['id']]) instance.volume_url = self.volume_url context["instance"] = instance - context["url"] = reverse(self.redirect_url) + context["url"] = get_url_with_pagination( + self.request, + project_tables.InstancesTable._meta.pagination_param, + project_tables.InstancesTable._meta.prev_pagination_param, + self.redirect_url) + context["actions"] = self._get_actions(instance) return context diff --git a/openstack_dashboard/test/views.py b/openstack_dashboard/test/views.py new file mode 100644 index 0000000000..66a1a6a505 --- /dev/null +++ b/openstack_dashboard/test/views.py @@ -0,0 +1,46 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import six + +from openstack_dashboard.test import helpers as test +from openstack_dashboard import views + + +class DashboardViewsTest(test.TestCase): + def test_get_url_with_pagination(self): + req = self.request + url_string = 'horizon:project:instances:index' + url = views.get_url_with_pagination(req, None, None, url_string, None) + self.assertEqual(six.text_type('/project/instances/'), url) + + def test_get_url_with_pagination_with_if(self): + req = self.request + url_string = 'horizon:project:instances:detail' + url = views.get_url_with_pagination(req, None, None, url_string, 'id') + self.assertEqual(six.text_type('/project/instances/id/'), url) + + def test_get_url_with_pagination_next(self): + req = self.request + url_string = 'horizon:project:instances:index' + req.GET.update({'next': 'id'}) + url = views.get_url_with_pagination( + req, 'next', None, url_string, None) + self.assertEqual(six.text_type('/project/instances/?next=id'), url) + + def test_get_url_with_pagination_prev(self): + req = self.request + url_string = 'horizon:project:instances:index' + req.GET.update({'prev': 'id'}) + url = views.get_url_with_pagination( + req, None, 'prev', url_string, None) + self.assertEqual(six.text_type('/project/instances/?prev=id'), url) diff --git a/openstack_dashboard/views.py b/openstack_dashboard/views.py index 97223b73bb..4861134566 100644 --- a/openstack_dashboard/views.py +++ b/openstack_dashboard/views.py @@ -13,8 +13,10 @@ # under the License. from django.conf import settings +from django.core import urlresolvers from django import shortcuts import django.views.decorators.vary +from six.moves import urllib import horizon from horizon import base @@ -59,3 +61,22 @@ def splash(request): if MESSAGES_PATH: notifications.process_message_notification(request, MESSAGES_PATH) return response + + +def get_url_with_pagination(request, marker_name, prev_marker_name, url_string, + object_id=None): + if object_id: + url = urlresolvers.reverse(url_string, args=(object_id,)) + else: + url = urlresolvers.reverse(url_string) + marker = request.GET.get(marker_name, None) + if marker: + return "{}?{}".format(url, + urllib.parse.urlencode({marker_name: marker})) + + prev_marker = request.GET.get(prev_marker_name, None) + if prev_marker: + return "{}?{}".format(url, + urllib.parse.urlencode({prev_marker_name: + prev_marker})) + return url