diff --git a/horizon/tables/__init__.py b/horizon/tables/__init__.py index 2a8840423d..c47bfa088e 100644 --- a/horizon/tables/__init__.py +++ b/horizon/tables/__init__.py @@ -31,6 +31,7 @@ from horizon.tables.views import MixedDataTableView from horizon.tables.views import MultiTableMixin from horizon.tables.views import MultiTableView from horizon.tables.views import PagedTableMixin +from horizon.tables.views import PagedTableWithPageMenu __all__ = [ @@ -50,4 +51,5 @@ __all__ = [ 'MultiTableMixin', 'MultiTableView', 'PagedTableMixin', + 'PagedTableWithPageMenu', ] diff --git a/horizon/tables/views.py b/horizon/tables/views.py index d52acf4d8f..e106a0431f 100644 --- a/horizon/tables/views.py +++ b/horizon/tables/views.py @@ -389,3 +389,47 @@ class PagedTableMixin(object): if marker: return marker, "desc" return None, "desc" + + +class PagedTableWithPageMenu(object): + def __init__(self, *args, **kwargs): + super(PagedTableWithPageMenu, self).__init__(*args, **kwargs) + self._current_page = 1 + self._number_of_pages = 0 + self._total_of_entries = 0 + self._page_size = 0 + + def handle_table(self, table): + name = table.name + self._tables[name]._meta.current_page = self.current_page + self._tables[name]._meta.number_of_pages = self.number_of_pages + return super(PagedTableWithPageMenu, self).handle_table(table) + + def has_prev_data(self, table): + return self._current_page > 1 + + def has_more_data(self, table): + return self._current_page < self._number_of_pages + + def current_page(self, table=None): + return self._current_page + + def number_of_pages(self, table=None): + return self._number_of_pages + + def current_offset(self, table): + return self._current_page * self._page_size + 1 + + def get_page_param(self, table): + try: + meta = self.table_class._meta + except AttributeError: + meta = self.table_classes[0]._meta + + return meta.pagination_param + + def _get_page_number(self): + page_number = self.request.GET.get(self.get_page_param(None), None) + if page_number: + return int(page_number) + return 1 diff --git a/horizon/templates/horizon/common/_data_table.html b/horizon/templates/horizon/common/_data_table.html index e63ae4ee45..c5f3dd7481 100644 --- a/horizon/templates/horizon/common/_data_table.html +++ b/horizon/templates/horizon/common/_data_table.html @@ -24,7 +24,11 @@ {% endif %} {% endblock table_breadcrumb %} {% if table.footer and rows %} - {% include "horizon/common/_data_table_pagination.html" %} + {% if table.number_of_pages is defined %} + {% include "horizon/common/_data_table_pagination.html" %} + {% else %} + {% include "horizon/common/_data_table_pagination_with_pages.html" %} + {% endif %} {% endif %} {% block table_columns %} {% if not table.is_browser_table %} @@ -72,7 +76,11 @@ {% endfor %} {% endif %} - {% include "horizon/common/_data_table_pagination.html" %} + {% if table.number_of_pages is defined %} + {% include "horizon/common/_data_table_pagination.html" %} + {% else %} + {% include "horizon/common/_data_table_pagination_with_pages.html" %} + {% endif %} {% endif %} {% endblock table_footer %} diff --git a/horizon/templates/horizon/common/_data_table_pagination_with_pages.html b/horizon/templates/horizon/common/_data_table_pagination_with_pages.html new file mode 100644 index 0000000000..4dacdeabbe --- /dev/null +++ b/horizon/templates/horizon/common/_data_table_pagination_with_pages.html @@ -0,0 +1,27 @@ +{% load i18n %} +{% load form_helpers %} + + + {% blocktrans count counter=rows|length trimmed %} + Displaying {{ counter }} item{% plural %} + Displaying {{ counter }} items{% endblocktrans %} + {% if table.has_prev_data or table.has_more_data %} + | + {% endif %} + {% if table.has_prev_data %} + {% trans "«« First" %} + {% trans "« Prev " %} + {% endif %} + {% for page in table.number_of_pages|get_range %} + {% if table.current_page == page %} + {{page}}  + {% else %} + {{page}}  + {% endif %} + {% endfor %} + {% if table.has_more_data %} + {% trans "Next »" %} + {% trans "Last »»" %} + {% endif %} + + \ No newline at end of file diff --git a/horizon/templatetags/form_helpers.py b/horizon/templatetags/form_helpers.py index 27b155344c..77eda593ef 100644 --- a/horizon/templatetags/form_helpers.py +++ b/horizon/templatetags/form_helpers.py @@ -80,3 +80,10 @@ def wrapper_classes(field): if is_multiple_checkbox(field): classes.append('multiple-checkbox') return ' '.join(classes) + + +@register.filter +def get_range(val): + if val: + return range(1, val + 1) + return [] diff --git a/openstack_dashboard/api/cinder.py b/openstack_dashboard/api/cinder.py index aff97c951a..2032114441 100644 --- a/openstack_dashboard/api/cinder.py +++ b/openstack_dashboard/api/cinder.py @@ -21,6 +21,7 @@ from __future__ import absolute_import import logging +import math from django.conf import settings from django.utils.translation import pgettext_lazy @@ -585,6 +586,42 @@ def volume_backup_list(request): return backups +@profiler.trace +def volume_backup_list_paged_with_page_menu(request, page_number=1, + sort_dir="desc"): + backups = [] + count = 0 + pages_count = 0 + page_size = utils.get_page_size(request) + c_client = cinderclient(request, '3.45') + + if c_client is None: + return backups, 0, count, pages_count + + if VERSIONS.active > 1: + offset = (page_number - 1) * page_size + sort = 'created_at:' + sort_dir + bkps, count = c_client.backups.list(limit=page_size, + sort=sort, + search_opts={'with_count': True, + 'offset': offset}) + if not bkps: + return backups, page_size, count, pages_count + + if isinstance(bkps[0], list): + bkps = bkps[0] + pages_count = int(math.ceil(float(count) / float(page_size))) + for b in bkps: + backups.append(VolumeBackup(b)) + + return backups, page_size, count, pages_count + else: + for b in c_client.backups.list(): + backups.append(VolumeBackup(b)) + + return backups, 0, count, pages_count + + @profiler.trace def volume_backup_list_paged(request, marker=None, paginate=False, sort_dir="desc"): diff --git a/openstack_dashboard/dashboards/project/backups/tables.py b/openstack_dashboard/dashboards/project/backups/tables.py index 0995596a38..4b7e32e69c 100644 --- a/openstack_dashboard/dashboards/project/backups/tables.py +++ b/openstack_dashboard/dashboards/project/backups/tables.py @@ -178,11 +178,19 @@ class BackupsTable(tables.DataTable): verbose_name=_("Snapshot"), link="horizon:project:snapshots:detail") + def current_page(self): + return self._meta.current_page() + + def number_of_pages(self): + return self._meta.number_of_pages() + + def get_pagination_string(self): + return '?%s=' % self._meta.pagination_param + class Meta(object): name = "volume_backups" verbose_name = _("Volume Backups") - pagination_param = 'backup_marker' - prev_pagination_param = 'prev_backup_marker' + pagination_param = 'page' status_columns = ("status",) row_class = UpdateRow table_actions = (DeleteBackup,) diff --git a/openstack_dashboard/dashboards/project/backups/tests.py b/openstack_dashboard/dashboards/project/backups/tests.py index 8193c03597..a3616fe0b5 100644 --- a/openstack_dashboard/dashboards/project/backups/tests.py +++ b/openstack_dashboard/dashboards/project/backups/tests.py @@ -28,11 +28,13 @@ INDEX_URL = reverse('horizon:project:backups:index') class VolumeBackupsViewTests(test.TestCase): @test.create_mocks({api.cinder: ('volume_list', 'volume_snapshot_list', - 'volume_backup_list_paged')}) - def _test_backups_index_paginated(self, marker, sort_dir, backups, url, - has_more, has_prev): - self.mock_volume_backup_list_paged.return_value = [backups, - has_more, has_prev] + 'volume_backup_list_paged_with_page_menu') + }) + def _test_backups_index_paginated(self, page_number, backups, + url, page_size, total_of_entries, + number_of_pages, has_prev, has_more): + self.mock_volume_backup_list_paged_with_page_menu.return_value = [ + backups, page_size, total_of_entries, number_of_pages] self.mock_volume_list.return_value = self.cinder_volumes.list() self.mock_volume_snapshot_list.return_value \ = self.cinder_volume_snapshots.list() @@ -41,9 +43,17 @@ class VolumeBackupsViewTests(test.TestCase): self.assertEqual(res.status_code, 200) self.assertTemplateUsed(res, 'horizon/common/_data_table_view.html') - self.mock_volume_backup_list_paged.assert_called_once_with( - test.IsHttpRequest(), marker=marker, sort_dir=sort_dir, - paginate=True) + self.assertEqual(has_more, + res.context_data['view'].has_more_data(None)) + self.assertEqual(has_prev, + res.context_data['view'].has_prev_data(None)) + self.assertEqual( + page_number, res.context_data['view'].current_page(None)) + self.assertEqual( + number_of_pages, res.context_data['view'].number_of_pages(None)) + self.mock_volume_backup_list_paged_with_page_menu.\ + assert_called_once_with(test.IsHttpRequest(), + page_number=page_number) self.mock_volume_list.assert_called_once_with(test.IsHttpRequest()) self.mock_volume_snapshot_list.assert_called_once_with( test.IsHttpRequest()) @@ -55,34 +65,38 @@ class VolumeBackupsViewTests(test.TestCase): expected_snapshosts = self.cinder_volume_snapshots.list() size = settings.API_RESULT_PAGE_SIZE base_url = INDEX_URL - next = backup_tables.BackupsTable._meta.pagination_param + number_of_pages = len(backups) + pag = backup_tables.BackupsTable._meta.pagination_param + page_number = 1 # get first page expected_backups = backups[:size] res = self._test_backups_index_paginated( - marker=None, sort_dir="desc", backups=expected_backups, - url=base_url, has_more=True, has_prev=False) + page_number=page_number, backups=expected_backups, url=base_url, + has_more=True, has_prev=False, page_size=size, + number_of_pages=number_of_pages, total_of_entries=number_of_pages) result = res.context['volume_backups_table'].data self.assertCountEqual(result, expected_backups) # get second page expected_backups = backups[size:2 * size] - marker = expected_backups[0].id - - url = base_url + "?%s=%s" % (next, marker) + page_number = 2 + url = base_url + "?%s=%s" % (pag, page_number) res = self._test_backups_index_paginated( - marker=marker, sort_dir="desc", backups=expected_backups, url=url, - has_more=True, has_prev=True) + page_number=page_number, backups=expected_backups, url=url, + has_more=True, has_prev=True, page_size=size, + number_of_pages=number_of_pages, total_of_entries=number_of_pages) result = res.context['volume_backups_table'].data self.assertCountEqual(result, expected_backups) self.assertEqual(result[0].snapshot.id, expected_snapshosts[1].id) # get last page expected_backups = backups[-size:] - marker = expected_backups[0].id - url = base_url + "?%s=%s" % (next, marker) + page_number = 3 + url = base_url + "?%s=%s" % (pag, page_number) res = self._test_backups_index_paginated( - marker=marker, sort_dir="desc", backups=expected_backups, url=url, - has_more=False, has_prev=True) + page_number=page_number, backups=expected_backups, url=url, + has_more=False, has_prev=True, page_size=size, + number_of_pages=number_of_pages, total_of_entries=number_of_pages) result = res.context['volume_backups_table'].data self.assertCountEqual(result, expected_backups) @@ -90,26 +104,29 @@ class VolumeBackupsViewTests(test.TestCase): def test_backups_index_paginated_prev_page(self): backups = self.cinder_volume_backups.list() size = settings.API_RESULT_PAGE_SIZE + number_of_pages = len(backups) base_url = INDEX_URL - prev = backup_tables.BackupsTable._meta.prev_pagination_param + pag = backup_tables.BackupsTable._meta.pagination_param # prev from some page expected_backups = backups[size:2 * size] - marker = expected_backups[0].id - url = base_url + "?%s=%s" % (prev, marker) + page_number = 2 + url = base_url + "?%s=%s" % (pag, page_number) res = self._test_backups_index_paginated( - marker=marker, sort_dir="asc", backups=expected_backups, url=url, - has_more=True, has_prev=True) + page_number=page_number, backups=expected_backups, url=url, + has_more=True, has_prev=True, page_size=size, + number_of_pages=number_of_pages, total_of_entries=number_of_pages) result = res.context['volume_backups_table'].data self.assertCountEqual(result, expected_backups) # back to first page expected_backups = backups[:size] - marker = expected_backups[0].id - url = base_url + "?%s=%s" % (prev, marker) + page_number = 1 + url = base_url + "?%s=%s" % (pag, page_number) res = self._test_backups_index_paginated( - marker=marker, sort_dir="asc", backups=expected_backups, url=url, - has_more=True, has_prev=False) + page_number=page_number, backups=expected_backups, url=url, + has_more=True, has_prev=False, page_size=size, + number_of_pages=number_of_pages, total_of_entries=number_of_pages) result = res.context['volume_backups_table'].data self.assertCountEqual(result, expected_backups) @@ -267,16 +284,20 @@ class VolumeBackupsViewTests(test.TestCase): @test.create_mocks({api.cinder: ('volume_list', 'volume_snapshot_list', - 'volume_backup_list_paged', + 'volume_backup_list_paged_with_page_menu', 'volume_backup_delete')}) def test_delete_volume_backup(self): vol_backups = self.cinder_volume_backups.list() volumes = self.cinder_volumes.list() backup = self.cinder_volume_backups.first() snapshots = self.cinder_volume_snapshots.list() + page_number = 1 + page_size = 1 + total_of_entries = 1 + number_of_pages = 1 - self.mock_volume_backup_list_paged.return_value = [vol_backups, - False, False] + self.mock_volume_backup_list_paged_with_page_menu.return_value = [ + vol_backups, page_size, total_of_entries, number_of_pages] self.mock_volume_list.return_value = volumes self.mock_volume_backup_delete.return_value = None self.mock_volume_snapshot_list.return_value = snapshots @@ -286,9 +307,9 @@ class VolumeBackupsViewTests(test.TestCase): self.assertRedirectsNoFollow(res, INDEX_URL) self.assertMessageCount(success=1) - self.mock_volume_backup_list_paged.assert_called_once_with( - test.IsHttpRequest(), marker=None, sort_dir='desc', - paginate=True) + self.mock_volume_backup_list_paged_with_page_menu.\ + assert_called_once_with(test.IsHttpRequest(), + page_number=page_number) self.mock_volume_list.assert_called_once_with(test.IsHttpRequest()) self.mock_volume_snapshot_list.assert_called_once_with( test.IsHttpRequest()) diff --git a/openstack_dashboard/dashboards/project/backups/views.py b/openstack_dashboard/dashboards/project/backups/views.py index 481308b875..af17c7298b 100644 --- a/openstack_dashboard/dashboards/project/backups/views.py +++ b/openstack_dashboard/dashboards/project/backups/views.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import logging + from django.urls import reverse from django.urls import reverse_lazy from django.utils.translation import ugettext_lazy as _ @@ -30,8 +32,10 @@ from openstack_dashboard.dashboards.project.backups \ from openstack_dashboard.dashboards.project.volumes \ import views as volume_views +LOG = logging.getLogger(__name__) -class BackupsView(tables.DataTableView, tables.PagedTableMixin, + +class BackupsView(tables.PagedTableWithPageMenu, tables.DataTableView, volume_views.VolumeTableMixIn): table_class = backup_tables.BackupsTable page_title = _("Volume Backups") @@ -41,11 +45,11 @@ class BackupsView(tables.DataTableView, tables.PagedTableMixin, def get_data(self): try: - marker, sort_dir = self._get_marker() - backups, self._has_more_data, self._has_prev_data = \ - api.cinder.volume_backup_list_paged( - self.request, marker=marker, sort_dir=sort_dir, - paginate=True) + self._current_page = self._get_page_number() + (backups, self._page_size, self._total_of_entries, + self._number_of_pages) = \ + api.cinder.volume_backup_list_paged_with_page_menu( + self.request, page_number=self._current_page) volumes = api.cinder.volume_list(self.request) volumes = dict((v.id, v) for v in volumes) snapshots = api.cinder.volume_snapshot_list(self.request) @@ -53,7 +57,8 @@ class BackupsView(tables.DataTableView, tables.PagedTableMixin, for backup in backups: backup.volume = volumes.get(backup.volume_id) backup.snapshot = snapshots.get(backup.snapshot_id) - except Exception: + except Exception as e: + LOG.exception(e) backups = [] exceptions.handle(self.request, _("Unable to retrieve " "volume backups."))