123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414 |
- import warnings
- from datetime import datetime
- from django.core.paginator import (
- EmptyPage, InvalidPage, PageNotAnInteger, Paginator,
- UnorderedObjectListWarning,
- )
- from django.test import SimpleTestCase, TestCase
- from .custom import ValidAdjacentNumsPaginator
- from .models import Article
- class PaginationTests(SimpleTestCase):
- """
- Tests for the Paginator and Page classes.
- """
- def check_paginator(self, params, output):
- """
- Helper method that instantiates a Paginator object from the passed
- params and then checks that its attributes match the passed output.
- """
- count, num_pages, page_range = output
- paginator = Paginator(*params)
- self.check_attribute('count', paginator, count, params)
- self.check_attribute('num_pages', paginator, num_pages, params)
- self.check_attribute('page_range', paginator, page_range, params, coerce=list)
- def check_attribute(self, name, paginator, expected, params, coerce=None):
- """
- Helper method that checks a single attribute and gives a nice error
- message upon test failure.
- """
- got = getattr(paginator, name)
- if coerce is not None:
- got = coerce(got)
- self.assertEqual(
- expected, got,
- "For '%s', expected %s but got %s. Paginator parameters were: %s"
- % (name, expected, got, params)
- )
- def test_paginator(self):
- """
- Tests the paginator attributes using varying inputs.
- """
- nine = [1, 2, 3, 4, 5, 6, 7, 8, 9]
- ten = nine + [10]
- eleven = ten + [11]
- tests = (
- # Each item is two tuples:
- # First tuple is Paginator parameters - object_list, per_page,
- # orphans, and allow_empty_first_page.
- # Second tuple is resulting Paginator attributes - count,
- # num_pages, and page_range.
- # Ten items, varying orphans, no empty first page.
- ((ten, 4, 0, False), (10, 3, [1, 2, 3])),
- ((ten, 4, 1, False), (10, 3, [1, 2, 3])),
- ((ten, 4, 2, False), (10, 2, [1, 2])),
- ((ten, 4, 5, False), (10, 2, [1, 2])),
- ((ten, 4, 6, False), (10, 1, [1])),
- # Ten items, varying orphans, allow empty first page.
- ((ten, 4, 0, True), (10, 3, [1, 2, 3])),
- ((ten, 4, 1, True), (10, 3, [1, 2, 3])),
- ((ten, 4, 2, True), (10, 2, [1, 2])),
- ((ten, 4, 5, True), (10, 2, [1, 2])),
- ((ten, 4, 6, True), (10, 1, [1])),
- # One item, varying orphans, no empty first page.
- (([1], 4, 0, False), (1, 1, [1])),
- (([1], 4, 1, False), (1, 1, [1])),
- (([1], 4, 2, False), (1, 1, [1])),
- # One item, varying orphans, allow empty first page.
- (([1], 4, 0, True), (1, 1, [1])),
- (([1], 4, 1, True), (1, 1, [1])),
- (([1], 4, 2, True), (1, 1, [1])),
- # Zero items, varying orphans, no empty first page.
- (([], 4, 0, False), (0, 0, [])),
- (([], 4, 1, False), (0, 0, [])),
- (([], 4, 2, False), (0, 0, [])),
- # Zero items, varying orphans, allow empty first page.
- (([], 4, 0, True), (0, 1, [1])),
- (([], 4, 1, True), (0, 1, [1])),
- (([], 4, 2, True), (0, 1, [1])),
- # Number if items one less than per_page.
- (([], 1, 0, True), (0, 1, [1])),
- (([], 1, 0, False), (0, 0, [])),
- (([1], 2, 0, True), (1, 1, [1])),
- ((nine, 10, 0, True), (9, 1, [1])),
- # Number if items equal to per_page.
- (([1], 1, 0, True), (1, 1, [1])),
- (([1, 2], 2, 0, True), (2, 1, [1])),
- ((ten, 10, 0, True), (10, 1, [1])),
- # Number if items one more than per_page.
- (([1, 2], 1, 0, True), (2, 2, [1, 2])),
- (([1, 2, 3], 2, 0, True), (3, 2, [1, 2])),
- ((eleven, 10, 0, True), (11, 2, [1, 2])),
- # Number if items one more than per_page with one orphan.
- (([1, 2], 1, 1, True), (2, 1, [1])),
- (([1, 2, 3], 2, 1, True), (3, 1, [1])),
- ((eleven, 10, 1, True), (11, 1, [1])),
- # Non-integer inputs
- ((ten, '4', 1, False), (10, 3, [1, 2, 3])),
- ((ten, '4', 1, False), (10, 3, [1, 2, 3])),
- ((ten, 4, '1', False), (10, 3, [1, 2, 3])),
- ((ten, 4, '1', False), (10, 3, [1, 2, 3])),
- )
- for params, output in tests:
- self.check_paginator(params, output)
- def test_invalid_page_number(self):
- """
- Invalid page numbers result in the correct exception being raised.
- """
- paginator = Paginator([1, 2, 3], 2)
- with self.assertRaises(InvalidPage):
- paginator.page(3)
- with self.assertRaises(PageNotAnInteger):
- paginator.validate_number(None)
- with self.assertRaises(PageNotAnInteger):
- paginator.validate_number('x')
- with self.assertRaises(PageNotAnInteger):
- paginator.validate_number(1.2)
- def test_float_integer_page(self):
- paginator = Paginator([1, 2, 3], 2)
- self.assertEqual(paginator.validate_number(1.0), 1)
- def test_no_content_allow_empty_first_page(self):
- # With no content and allow_empty_first_page=True, 1 is a valid page number
- paginator = Paginator([], 2)
- self.assertEqual(paginator.validate_number(1), 1)
- def test_paginate_misc_classes(self):
- class CountContainer:
- def count(self):
- return 42
- # Paginator can be passed other objects with a count() method.
- paginator = Paginator(CountContainer(), 10)
- self.assertEqual(42, paginator.count)
- self.assertEqual(5, paginator.num_pages)
- self.assertEqual([1, 2, 3, 4, 5], list(paginator.page_range))
- # Paginator can be passed other objects that implement __len__.
- class LenContainer:
- def __len__(self):
- return 42
- paginator = Paginator(LenContainer(), 10)
- self.assertEqual(42, paginator.count)
- self.assertEqual(5, paginator.num_pages)
- self.assertEqual([1, 2, 3, 4, 5], list(paginator.page_range))
- def test_count_does_not_silence_attribute_error(self):
- class AttributeErrorContainer:
- def count(self):
- raise AttributeError('abc')
- with self.assertRaisesMessage(AttributeError, 'abc'):
- Paginator(AttributeErrorContainer(), 10).count
- def test_count_does_not_silence_type_error(self):
- class TypeErrorContainer:
- def count(self):
- raise TypeError('abc')
- with self.assertRaisesMessage(TypeError, 'abc'):
- Paginator(TypeErrorContainer(), 10).count
- def check_indexes(self, params, page_num, indexes):
- """
- Helper method that instantiates a Paginator object from the passed
- params and then checks that the start and end indexes of the passed
- page_num match those given as a 2-tuple in indexes.
- """
- paginator = Paginator(*params)
- if page_num == 'first':
- page_num = 1
- elif page_num == 'last':
- page_num = paginator.num_pages
- page = paginator.page(page_num)
- start, end = indexes
- msg = ("For %s of page %s, expected %s but got %s. Paginator parameters were: %s")
- self.assertEqual(start, page.start_index(), msg % ('start index', page_num, start, page.start_index(), params))
- self.assertEqual(end, page.end_index(), msg % ('end index', page_num, end, page.end_index(), params))
- def test_page_indexes(self):
- """
- Paginator pages have the correct start and end indexes.
- """
- ten = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
- tests = (
- # Each item is three tuples:
- # First tuple is Paginator parameters - object_list, per_page,
- # orphans, and allow_empty_first_page.
- # Second tuple is the start and end indexes of the first page.
- # Third tuple is the start and end indexes of the last page.
- # Ten items, varying per_page, no orphans.
- ((ten, 1, 0, True), (1, 1), (10, 10)),
- ((ten, 2, 0, True), (1, 2), (9, 10)),
- ((ten, 3, 0, True), (1, 3), (10, 10)),
- ((ten, 5, 0, True), (1, 5), (6, 10)),
- # Ten items, varying per_page, with orphans.
- ((ten, 1, 1, True), (1, 1), (9, 10)),
- ((ten, 1, 2, True), (1, 1), (8, 10)),
- ((ten, 3, 1, True), (1, 3), (7, 10)),
- ((ten, 3, 2, True), (1, 3), (7, 10)),
- ((ten, 3, 4, True), (1, 3), (4, 10)),
- ((ten, 5, 1, True), (1, 5), (6, 10)),
- ((ten, 5, 2, True), (1, 5), (6, 10)),
- ((ten, 5, 5, True), (1, 10), (1, 10)),
- # One item, varying orphans, no empty first page.
- (([1], 4, 0, False), (1, 1), (1, 1)),
- (([1], 4, 1, False), (1, 1), (1, 1)),
- (([1], 4, 2, False), (1, 1), (1, 1)),
- # One item, varying orphans, allow empty first page.
- (([1], 4, 0, True), (1, 1), (1, 1)),
- (([1], 4, 1, True), (1, 1), (1, 1)),
- (([1], 4, 2, True), (1, 1), (1, 1)),
- # Zero items, varying orphans, allow empty first page.
- (([], 4, 0, True), (0, 0), (0, 0)),
- (([], 4, 1, True), (0, 0), (0, 0)),
- (([], 4, 2, True), (0, 0), (0, 0)),
- )
- for params, first, last in tests:
- self.check_indexes(params, 'first', first)
- self.check_indexes(params, 'last', last)
- # When no items and no empty first page, we should get EmptyPage error.
- with self.assertRaises(EmptyPage):
- self.check_indexes(([], 4, 0, False), 1, None)
- with self.assertRaises(EmptyPage):
- self.check_indexes(([], 4, 1, False), 1, None)
- with self.assertRaises(EmptyPage):
- self.check_indexes(([], 4, 2, False), 1, None)
- def test_page_sequence(self):
- """
- A paginator page acts like a standard sequence.
- """
- eleven = 'abcdefghijk'
- page2 = Paginator(eleven, per_page=5, orphans=1).page(2)
- self.assertEqual(len(page2), 6)
- self.assertIn('k', page2)
- self.assertNotIn('a', page2)
- self.assertEqual(''.join(page2), 'fghijk')
- self.assertEqual(''.join(reversed(page2)), 'kjihgf')
- def test_get_page_hook(self):
- """
- A Paginator subclass can use the ``_get_page`` hook to
- return an alternative to the standard Page class.
- """
- eleven = 'abcdefghijk'
- paginator = ValidAdjacentNumsPaginator(eleven, per_page=6)
- page1 = paginator.page(1)
- page2 = paginator.page(2)
- self.assertIsNone(page1.previous_page_number())
- self.assertEqual(page1.next_page_number(), 2)
- self.assertEqual(page2.previous_page_number(), 1)
- self.assertIsNone(page2.next_page_number())
- def test_page_range_iterator(self):
- """
- Paginator.page_range should be an iterator.
- """
- self.assertIsInstance(Paginator([1, 2, 3], 2).page_range, type(range(0)))
- def test_get_page(self):
- """
- Paginator.get_page() returns a valid page even with invalid page
- arguments.
- """
- paginator = Paginator([1, 2, 3], 2)
- page = paginator.get_page(1)
- self.assertEqual(page.number, 1)
- self.assertEqual(page.object_list, [1, 2])
- # An empty page returns the last page.
- self.assertEqual(paginator.get_page(3).number, 2)
- # Non-integer page returns the first page.
- self.assertEqual(paginator.get_page(None).number, 1)
- def test_get_page_empty_object_list(self):
- """Paginator.get_page() with an empty object_list."""
- paginator = Paginator([], 2)
- # An empty page returns the last page.
- self.assertEqual(paginator.get_page(1).number, 1)
- self.assertEqual(paginator.get_page(2).number, 1)
- # Non-integer page returns the first page.
- self.assertEqual(paginator.get_page(None).number, 1)
- def test_get_page_empty_object_list_and_allow_empty_first_page_false(self):
- """
- Paginator.get_page() raises EmptyPage if allow_empty_first_page=False
- and object_list is empty.
- """
- paginator = Paginator([], 2, allow_empty_first_page=False)
- with self.assertRaises(EmptyPage):
- paginator.get_page(1)
- def test_paginator_iteration(self):
- paginator = Paginator([1, 2, 3], 2)
- page_iterator = iter(paginator)
- for page, expected in enumerate(([1, 2], [3]), start=1):
- with self.subTest(page=page):
- self.assertEqual(expected, list(next(page_iterator)))
- class ModelPaginationTests(TestCase):
- """
- Test pagination with Django model instances
- """
- @classmethod
- def setUpTestData(cls):
- # Prepare a list of objects for pagination.
- for x in range(1, 10):
- a = Article(headline='Article %s' % x, pub_date=datetime(2005, 7, 29))
- a.save()
- def test_first_page(self):
- paginator = Paginator(Article.objects.order_by('id'), 5)
- p = paginator.page(1)
- self.assertEqual("<Page 1 of 2>", str(p))
- self.assertQuerysetEqual(p.object_list, [
- "<Article: Article 1>",
- "<Article: Article 2>",
- "<Article: Article 3>",
- "<Article: Article 4>",
- "<Article: Article 5>"
- ])
- self.assertTrue(p.has_next())
- self.assertFalse(p.has_previous())
- self.assertTrue(p.has_other_pages())
- self.assertEqual(2, p.next_page_number())
- with self.assertRaises(InvalidPage):
- p.previous_page_number()
- self.assertEqual(1, p.start_index())
- self.assertEqual(5, p.end_index())
- def test_last_page(self):
- paginator = Paginator(Article.objects.order_by('id'), 5)
- p = paginator.page(2)
- self.assertEqual("<Page 2 of 2>", str(p))
- self.assertQuerysetEqual(p.object_list, [
- "<Article: Article 6>",
- "<Article: Article 7>",
- "<Article: Article 8>",
- "<Article: Article 9>"
- ])
- self.assertFalse(p.has_next())
- self.assertTrue(p.has_previous())
- self.assertTrue(p.has_other_pages())
- with self.assertRaises(InvalidPage):
- p.next_page_number()
- self.assertEqual(1, p.previous_page_number())
- self.assertEqual(6, p.start_index())
- self.assertEqual(9, p.end_index())
- def test_page_getitem(self):
- """
- Tests proper behavior of a paginator page __getitem__ (queryset
- evaluation, slicing, exception raised).
- """
- paginator = Paginator(Article.objects.order_by('id'), 5)
- p = paginator.page(1)
- # Make sure object_list queryset is not evaluated by an invalid __getitem__ call.
- # (this happens from the template engine when using eg: {% page_obj.has_previous %})
- self.assertIsNone(p.object_list._result_cache)
- msg = 'Page indices must be integers or slices, not str.'
- with self.assertRaisesMessage(TypeError, msg):
- p['has_previous']
- self.assertIsNone(p.object_list._result_cache)
- self.assertNotIsInstance(p.object_list, list)
- # Make sure slicing the Page object with numbers and slice objects work.
- self.assertEqual(p[0], Article.objects.get(headline='Article 1'))
- self.assertQuerysetEqual(p[slice(2)], [
- "<Article: Article 1>",
- "<Article: Article 2>",
- ]
- )
- # After __getitem__ is called, object_list is a list
- self.assertIsInstance(p.object_list, list)
- def test_paginating_unordered_queryset_raises_warning(self):
- msg = (
- "Pagination may yield inconsistent results with an unordered "
- "object_list: <class 'pagination.models.Article'> QuerySet."
- )
- with self.assertWarnsMessage(UnorderedObjectListWarning, msg) as cm:
- Paginator(Article.objects.all(), 5)
- # The warning points at the Paginator caller (i.e. the stacklevel
- # is appropriate).
- self.assertEqual(cm.filename, __file__)
- def test_paginating_empty_queryset_does_not_warn(self):
- with warnings.catch_warnings(record=True) as recorded:
- Paginator(Article.objects.none(), 5)
- self.assertEqual(len(recorded), 0)
- def test_paginating_unordered_object_list_raises_warning(self):
- """
- Unordered object list warning with an object that has an ordered
- attribute but not a model attribute.
- """
- class ObjectList:
- ordered = False
- object_list = ObjectList()
- msg = (
- "Pagination may yield inconsistent results with an unordered "
- "object_list: {!r}.".format(object_list)
- )
- with self.assertWarnsMessage(UnorderedObjectListWarning, msg):
- Paginator(object_list, 5)
|