diff --git a/springboard/defaults.ini b/springboard/defaults.ini index 1282bc3..53de605 100644 --- a/springboard/defaults.ini +++ b/springboard/defaults.ini @@ -20,6 +20,7 @@ jinja2.filters = markdown = springboard.filters:markdown_filter display_language_name = springboard.filters:display_language_name_filter language_direction = springboard.filters:language_direction_filter + paginate = springboard.filters:paginate_filter unicorehub.host = unicorehub.app_id = diff --git a/springboard/filters.py b/springboard/filters.py index 081dcd5..0aed351 100644 --- a/springboard/filters.py +++ b/springboard/filters.py @@ -11,6 +11,8 @@ from babel import Locale from pycountry import languages +from springboard.utils import Paginator + # known right-to-left language codes KNOWN_RTL_LANGUAGES = {"urd", "ara", "arc", "per", "heb", "kur", "yid"} @@ -53,3 +55,7 @@ def language_direction_filter(locale): if language_code in KNOWN_RTL_LANGUAGES: return 'rtl' return 'ltr' + + +def paginate_filter(results, page, results_per_page=10, slider_value=5): + return Paginator(results, page, results_per_page, slider_value) diff --git a/springboard/tests/test_filters.py b/springboard/tests/test_filters.py index 4177f50..6b51aa7 100644 --- a/springboard/tests/test_filters.py +++ b/springboard/tests/test_filters.py @@ -7,7 +7,7 @@ from springboard.tests import SpringboardTestCase from springboard.filters import ( format_date_filter, thumbor_filter, markdown_filter, - language_direction_filter) + language_direction_filter, paginate_filter) class TestFilters(SpringboardTestCase): @@ -58,3 +58,10 @@ def test_markdown_filter_none(self): def test_language_direction_filter(self): self.assertEqual(language_direction_filter('eng_GB'), 'ltr') self.assertEqual(language_direction_filter('urd_PK'), 'rtl') + + def test_paginate_filter(self): + paginator = paginate_filter([1, 2, 3, 4, 5], 2, + results_per_page=1, + slider_value=2) + self.assertEqual(paginator.get_page(), [3]) + self.assertEqual(paginator.page_numbers(), [1, 2, 3]) diff --git a/springboard/tests/test_utils.py b/springboard/tests/test_utils.py index 35faa83..997186f 100644 --- a/springboard/tests/test_utils.py +++ b/springboard/tests/test_utils.py @@ -1,6 +1,13 @@ from unittest import TestCase -from springboard.utils import parse_repo_name, config_list, config_dict +from mock import patch + +from unicore.content.models import Category +from elasticgit.search import SM + +from springboard.tests.base import SpringboardTestCase +from springboard.utils import ( + parse_repo_name, config_list, config_dict, Paginator) class TestUtils(TestCase): @@ -36,3 +43,103 @@ def test_config_dict(self): 'elastic': 'search', }, config_dict('foo=bar\ngum=tree\nelastic=search\n')) self.assertEqual({}, config_dict('')) + + +class TestPaginator(TestCase): + + def mk_paginator(self, results, page, **kwargs): + return Paginator(results, page, **kwargs) + + def test_first_page(self): + paginator = self.mk_paginator(range(100), 0) + self.assertTrue(paginator.has_next_page()) + self.assertFalse(paginator.has_previous_page()) + self.assertEqual(paginator.total_pages(), 10) + self.assertEqual(paginator.page_numbers(), [0, 1, 2, 3, 4]) + self.assertFalse(paginator.needs_start_ellipsis()) + self.assertTrue(paginator.needs_end_ellipsis()) + self.assertEqual(paginator.page_numbers_left(), []) + self.assertEqual(paginator.page_numbers_right(), [1, 2, 3, 4]) + + def test_last_page(self): + paginator = self.mk_paginator(range(100), 9) + self.assertFalse(paginator.has_next_page()) + self.assertTrue(paginator.has_previous_page()) + self.assertEqual(paginator.total_pages(), 10) + self.assertEqual(paginator.page_numbers(), [5, 6, 7, 8, 9]) + self.assertTrue(paginator.needs_start_ellipsis()) + self.assertFalse(paginator.needs_end_ellipsis()) + self.assertEqual(paginator.page_numbers_left(), [5, 6, 7, 8]) + self.assertEqual(paginator.page_numbers_right(), []) + + def test_middle_page(self): + paginator = self.mk_paginator(range(100), 4) + self.assertTrue(paginator.has_next_page()) + self.assertTrue(paginator.has_previous_page()) + self.assertEqual(paginator.total_pages(), 10) + self.assertEqual(paginator.page_numbers(), [2, 3, 4, 5, 6]) + self.assertTrue(paginator.needs_start_ellipsis()) + self.assertTrue(paginator.needs_end_ellipsis()) + self.assertEqual(paginator.page_numbers_left(), [2, 3]) + self.assertEqual(paginator.page_numbers_right(), [5, 6]) + + def test_show_start(self): + paginator = self.mk_paginator(range(100), 3) + self.assertTrue(paginator.show_start()) + self.assertFalse(paginator.needs_start_ellipsis()) + self.assertEqual(paginator.page_numbers_left(), [1, 2]) + self.assertEqual(paginator.page_numbers_right(), [4, 5]) + + def test_show_end(self): + paginator = self.mk_paginator(range(100), 7) + self.assertTrue(paginator.show_start()) + self.assertTrue(paginator.needs_start_ellipsis()) + self.assertEqual(paginator.page_numbers(), [5, 6, 7, 8, 9]) + self.assertEqual(paginator.page_numbers_left(), [5, 6]) + self.assertEqual(paginator.page_numbers_right(), [8, 9]) + self.assertFalse(paginator.show_end()) + self.assertFalse(paginator.needs_end_ellipsis()) + + def test_show_end_not_ellipsis(self): + paginator = self.mk_paginator(range(100), 6) + self.assertTrue(paginator.show_start()) + self.assertTrue(paginator.needs_start_ellipsis()) + self.assertEqual(paginator.page_numbers(), [4, 5, 6, 7, 8]) + self.assertEqual(paginator.page_numbers_left(), [4, 5]) + self.assertEqual(paginator.page_numbers_right(), [7, 8]) + self.assertTrue(paginator.show_end()) + self.assertFalse(paginator.needs_end_ellipsis()) + + def test_small_result_set(self): + paginator = self.mk_paginator(range(39), 0) + self.assertFalse(paginator.show_start()) + self.assertFalse(paginator.needs_start_ellipsis()) + self.assertFalse(paginator.show_end()) + self.assertFalse(paginator.needs_end_ellipsis()) + self.assertEqual(paginator.page_numbers_left(), []) + self.assertEqual(paginator.page_numbers_right(), [1, 2, 3]) + + def test_large_end_result_set(self): + paginator = self.mk_paginator(range(133), 12) + self.assertEqual(paginator.page_numbers(), [9, 10, 11, 12, 13]) + self.assertEqual(paginator.page_numbers_left(), [9, 10, 11]) + self.assertEqual(paginator.page_numbers_right(), [13]) + self.assertFalse(paginator.show_end()) + self.assertFalse(paginator.needs_end_ellipsis()) + + +class TestPaginatorWithESResults(TestPaginator, SpringboardTestCase): + + def mk_paginator(self, results, page, **kwargs): + workspace = self.mk_workspace() + patch_count = patch.object( + SM, 'count', return_value=len(results)) + patch_count.start() + self.addCleanup(patch_count.stop) + results = SM(Category, in_=[workspace.working_dir]) + return Paginator(results, page, **kwargs) + + def test_get_page(self): + paginator = self.mk_paginator(range(10), 0) + page = paginator.get_page() + self.assertIsInstance(page, SM) diff --git a/springboard/utils.py b/springboard/utils.py index e881f59..cbf9df5 100644 --- a/springboard/utils.py +++ b/springboard/utils.py @@ -1,6 +1,9 @@ import re from functools import wraps from urlparse import urlparse +import math + +from elasticutils import S def parse_repo_name(repo_url): @@ -73,3 +76,97 @@ def config_dict(data): """ lines = config_list(data) return dict(re.split('\s*=\s*', value) for value in lines) + + +class Paginator(object): + """ + A thing that helps us page through result sets + + :param iterable results: + The iterable of objects to paginate. + :param int page: + The page number, zero-based. + :param int results_per_page: + The number of objects in each page. + :param int slider_value: + The number of page numbers to display, excluding the current page. + + """ + + def __init__(self, results, page, results_per_page=10, slider_value=5): + self.results = results + self.page = page + self.results_per_page = results_per_page + self.slider_value = slider_value + self.buffer_value = self.slider_value / 2 + + def total_count(self): + if isinstance(self.results, S): + return self.results.count() + return len(self.results) + + def get_page(self): + return self.results[self.page * self.results_per_page: + (self.page + 1) * self.results_per_page] + + def has_next_page(self): + return ((self.page + 1) * self.results_per_page) < self.total_count() + + def has_previous_page(self): + return self.page + + def total_pages(self): + return int( + math.ceil( + float(self.total_count()) / float(self.results_per_page))) + + def page_numbers(self): + if (self.page - self.buffer_value) < 0: + return [page_number + for page_number in range( + 0, min([self.slider_value, self.total_pages()]))] + elif (self.page + self.buffer_value) >= self.total_pages(): + return [page_number + for page_number in range( + max((self.total_pages() - self.slider_value), 0), + self.total_pages()) + ] + else: + return range(self.page - self.buffer_value, + self.page + self.buffer_value + 1) + + def page_numbers_left(self): + page_numbers = self.page_numbers() + if not any(page_numbers): + return False + return page_numbers[:page_numbers.index(self.page)] + + def page_numbers_right(self): + page_numbers = self.page_numbers() + if not any(page_numbers): + return False + return page_numbers[page_numbers.index(self.page) + 1:] + + def needs_start_ellipsis(self): + page_numbers = self.page_numbers() + if not any(page_numbers): + return False + return page_numbers[0] > 1 + + def needs_end_ellipsis(self): + page_numbers = self.page_numbers() + if not any(page_numbers): + return False + return page_numbers[-1] < (self.total_pages() - 2) + + def show_start(self): + page_numbers = self.page_numbers() + if not any(page_numbers): + return False + return page_numbers[0] > 0 + + def show_end(self): + page_numbers = self.page_numbers() + if not any(page_numbers): + return False + return page_numbers[-1] < self.total_pages() - 1