summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2014-03-21 14:43:10 +0000
committerGerrit Code Review <review@openstack.org>2014-03-21 14:43:10 +0000
commit9dba2cfa41eb20fabc9d68924229812415225053 (patch)
tree7d62ff878f25e47c7bccd47b3c00dde307d50585
parent537dbacdcc316a60fe7c456d9a96f773c9c73948 (diff)
parente95a04733d463658f3ca87e9df213b8e1dd0326d (diff)
downloadhorizon-9dba2cfa41eb20fabc9d68924229812415225053.tar.gz
Merge "Selected instances are not deleted with pagination"
-rw-r--r--horizon/tables/base.py10
-rw-r--r--horizon/templates/horizon/common/_data_table.html2
-rw-r--r--openstack_dashboard/dashboards/project/containers/tables.py22
-rw-r--r--openstack_dashboard/dashboards/project/instances/tests.py91
4 files changed, 124 insertions, 1 deletions
diff --git a/horizon/tables/base.py b/horizon/tables/base.py
index b6c3dbe2b..10aaa98db 100644
--- a/horizon/tables/base.py
+++ b/horizon/tables/base.py
@@ -1161,6 +1161,16 @@ class DataTable(object):
"""
return self.request.get_full_path().partition('?')[0]
+ def get_full_url(self):
+ """Returns the full URL path for this table.
+
+ This is used for the POST action attribute on the form element
+ wrapping the table. We use this method to persist the
+ pagination marker.
+
+ """
+ return self.request.get_full_path()
+
def get_empty_message(self):
"""Returns the message to be displayed when there is no data."""
return self._no_data_message
diff --git a/horizon/templates/horizon/common/_data_table.html b/horizon/templates/horizon/common/_data_table.html
index d8a26370b..592364c96 100644
--- a/horizon/templates/horizon/common/_data_table.html
+++ b/horizon/templates/horizon/common/_data_table.html
@@ -1,7 +1,7 @@
{% load i18n %}
{% with table.needs_form_wrapper as needs_form_wrapper %}
<div class="table_wrapper">
- {% if needs_form_wrapper %}<form action="{{ table.get_absolute_url }}" method="POST">{% csrf_token %}{% endif %}
+ {% if needs_form_wrapper %}<form action="{{ table.get_full_url }}" method="POST">{% csrf_token %}{% endif %}
{% with columns=table.get_columns rows=table.get_rows %}
{% block table %}
<table id="{{ table.name }}" class="table table-bordered table-striped datatable">
diff --git a/openstack_dashboard/dashboards/project/containers/tables.py b/openstack_dashboard/dashboards/project/containers/tables.py
index c8acc3c2a..a14c880b0 100644
--- a/openstack_dashboard/dashboards/project/containers/tables.py
+++ b/openstack_dashboard/dashboards/project/containers/tables.py
@@ -269,6 +269,17 @@ class ContainersTable(tables.DataTable):
url = super(ContainersTable, self).get_absolute_url()
return http.urlquote(url)
+ def get_full_url(self):
+ """Returns the encoded absolute URL path with its query string.
+
+ This is used for the POST action attribute on the form element
+ wrapping the table. We use this method to persist the
+ pagination marker.
+
+ """
+ url = super(ContainersTable, self).get_full_url()
+ return http.urlquote(url)
+
class ViewObject(tables.LinkAction):
name = "view"
@@ -416,3 +427,14 @@ class ObjectsTable(tables.DataTable):
def get_absolute_url(self):
url = super(ObjectsTable, self).get_absolute_url()
return http.urlquote(url)
+
+ def get_full_url(self):
+ """Returns the encoded absolute URL path with its query string.
+
+ This is used for the POST action attribute on the form element
+ wrapping the table. We use this method to persist the
+ pagination marker.
+
+ """
+ url = super(ObjectsTable, self).get_full_url()
+ return http.urlquote(url)
diff --git a/openstack_dashboard/dashboards/project/instances/tests.py b/openstack_dashboard/dashboards/project/instances/tests.py
index 6b4fce762..32cb13f0c 100644
--- a/openstack_dashboard/dashboards/project/instances/tests.py
+++ b/openstack_dashboard/dashboards/project/instances/tests.py
@@ -21,6 +21,7 @@
import json
import uuid
+from django.conf import settings
from django.core.urlresolvers import reverse
from django import http
from django.test import utils as test_utils
@@ -2646,6 +2647,96 @@ class InstanceTests(test.TestCase):
disk_config='AUTO')
self.assertRedirectsNoFollow(res, INDEX_URL)
+ @test_utils.override_settings(API_RESULT_PAGE_SIZE=2)
+ @test.create_stubs({api.nova: ('flavor_list',
+ 'server_list',
+ 'tenant_absolute_limits',
+ 'extension_supported',),
+ api.glance: ('image_list_detailed',),
+ api.network:
+ ('floating_ip_simple_associate_supported',),
+ })
+ def test_index_form_action_with_pagination(self):
+ """The form action on the next page should have marker
+ object from the previous page last element.
+ """
+ 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)) \
+ .MultipleTimes().AndReturn(self.flavors.list())
+ api.glance.image_list_detailed(IgnoreArg()) \
+ .MultipleTimes().AndReturn((self.images.list(), False))
+
+ search_opts = {'marker': None, 'paginate': True}
+ api.nova.server_list(IsA(http.HttpRequest), search_opts=search_opts) \
+ .AndReturn([servers[:page_size], True])
+ api.nova.server_list(IsA(http.HttpRequest), search_opts={
+ 'marker': servers[page_size - 1].id, 'paginate': True}) \
+ .AndReturn([servers[page_size:], False])
+
+ api.nova.tenant_absolute_limits(IsA(http.HttpRequest), reserved=True) \
+ .MultipleTimes().AndReturn(self.limits['absolute'])
+ api.network.floating_ip_simple_associate_supported(
+ IsA(http.HttpRequest)).MultipleTimes().AndReturn(True)
+
+ self.mox.ReplayAll()
+
+ res = self.client.get(INDEX_URL)
+ self.assertTemplateUsed(res, 'project/instances/index.html')
+ # get first page with 2 items
+ self.assertEqual(len(res.context['instances_table'].data), page_size)
+
+ # update INDEX_URL with marker object
+ next_page_url = "?".join([reverse('horizon:project:instances:index'),
+ "=".join([tables.InstancesTable._meta.pagination_param,
+ servers[page_size - 1].id])])
+ form_action = 'action="%s"' % next_page_url
+
+ res = self.client.get(next_page_url)
+ # get next page with remaining items (item 3)
+ self.assertEqual(len(res.context['instances_table'].data), 1)
+ # ensure that marker object exists in form action
+ self.assertContains(res, form_action, count=1)
+
+ @test_utils.override_settings(API_RESULT_PAGE_SIZE=2)
+ @test.create_stubs({api.nova: ('server_list',
+ 'flavor_list',
+ 'server_delete',),
+ api.glance: ('image_list_detailed',),
+ api.network: ('servers_update_addresses',)})
+ def test_terminate_instance_with_pagination(self):
+ """Instance should be deleted from
+ the next page.
+ """
+ page_size = getattr(settings, 'API_RESULT_PAGE_SIZE', 2)
+ servers = self.servers.list()[:3]
+ server = servers[-1]
+
+ search_opts = {'marker': servers[page_size - 1].id, 'paginate': True}
+ api.nova.server_list(IsA(http.HttpRequest), search_opts=search_opts) \
+ .AndReturn([servers[page_size:], False])
+ api.network.servers_update_addresses(IsA(http.HttpRequest),
+ servers[page_size:])
+ api.nova.flavor_list(IgnoreArg()).AndReturn(self.flavors.list())
+ api.glance.image_list_detailed(IgnoreArg()) \
+ .AndReturn((self.images.list(), False))
+ api.nova.server_delete(IsA(http.HttpRequest), server.id)
+ self.mox.ReplayAll()
+
+ # update INDEX_URL with marker object
+ next_page_url = "?".join([reverse('horizon:project:instances:index'),
+ "=".join([tables.InstancesTable._meta.pagination_param,
+ servers[page_size - 1].id])])
+ formData = {'action': 'instances__terminate__%s' % server.id}
+ res = self.client.post(next_page_url, formData)
+
+ self.assertRedirectsNoFollow(res, next_page_url)
+ self.assertMessageCount(success=1)
+
class InstanceAjaxTests(test.TestCase):
@test.create_stubs({api.nova: ("server_get",