Skip to content

Commit cdfecd0

Browse files
committed
Updated new pagination feature.
1 parent 55cc528 commit cdfecd0

31 files changed

+395
-146
lines changed
File renamed without changes.

base/exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class IncorectLookupParameter(Exception):
2+
"""
3+
Raised when a query parameter contains an incorrect value.
4+
"""
5+
6+
pass

base/migrations/__init__.py

Whitespace-only changes.

base/pagination.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from django.core.paginator import InvalidPage, Paginator
2+
3+
from .exceptions import IncorectLookupParameter
4+
5+
PAGE_VAR = "page"
6+
7+
8+
class Pagination:
9+
def __init__(
10+
self,
11+
request,
12+
model,
13+
queryset,
14+
list_per_page,
15+
):
16+
self.model = model
17+
self.opts = model._meta
18+
self.queryset = queryset
19+
self.list_per_page = list_per_page
20+
try:
21+
# Get the current page from the query string.
22+
self.page_num = int(request.GET.get(PAGE_VAR, 1))
23+
except ValueError:
24+
self.page_num = 1
25+
self.params = dict(request.GET.lists())
26+
self.setup()
27+
28+
@property
29+
def page_range(self):
30+
"""
31+
Returns the full range of pages.
32+
"""
33+
return (
34+
self.paginator.get_elided_page_range(self.page_num)
35+
if self.multi_page
36+
else []
37+
)
38+
39+
def setup(self):
40+
paginator = Paginator(self.queryset, self.list_per_page)
41+
result_count = paginator.count
42+
# Determine use pagination.
43+
multi_page = result_count > self.list_per_page
44+
45+
self.result_count = result_count
46+
self.multi_page = multi_page
47+
self.paginator = paginator
48+
self.page = paginator.get_page(self.page_num)
49+
50+
def get_objects(self):
51+
if not self.multi_page:
52+
result_list = self.queryset._clone()
53+
else:
54+
try:
55+
result_list = self.paginator.page(self.page_num).object_list
56+
except InvalidPage:
57+
raise IncorectLookupParameter
58+
return result_list

base/templatetags/__init__.py

Whitespace-only changes.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from collections.abc import Iterable, Mapping
2+
3+
from django import template
4+
from django.http import QueryDict
5+
from django.template.exceptions import TemplateSyntaxError
6+
7+
register = template.Library()
8+
9+
10+
# This template tag is scheduled to be added in Django 6.0.
11+
# Imported for use before the release of Django 6.0.
12+
@register.simple_tag(name="querystring", takes_context=True)
13+
def querystring(context, *args, **kwargs):
14+
"""
15+
Build a query string using `args` and `kwargs` arguments.
16+
17+
This tag constructs a new query string by adding, removing, or modifying
18+
parameters from the given positional and keyword arguments. Positional
19+
arguments must be mappings (such as `QueryDict` or `dict`), and
20+
`request.GET` is used as the starting point if `args` is empty.
21+
22+
Keyword arguments are treated as an extra, final mapping. These mappings
23+
are processed sequentially, with later arguments taking precedence.
24+
25+
A query string prefixed with `?` is returned.
26+
27+
Raise TemplateSyntaxError if a positional argument is not a mapping or if
28+
keys are not strings.
29+
30+
For example::
31+
32+
{# Set a parameter on top of `request.GET` #}
33+
{% querystring foo=3 %}
34+
35+
{# Remove a key from `request.GET` #}
36+
{% querystring foo=None %}
37+
38+
{# Use with pagination #}
39+
{% querystring page=page_obj.next_page_number %}
40+
41+
{# Use a custom ``QueryDict`` #}
42+
{% querystring my_query_dict foo=3 %}
43+
44+
{# Use multiple positional and keyword arguments #}
45+
{% querystring my_query_dict my_dict foo=3 bar=None %}
46+
"""
47+
if not args:
48+
args = [context.request.GET]
49+
params = QueryDict(mutable=True)
50+
for d in [*args, kwargs]:
51+
if not isinstance(d, Mapping):
52+
raise TemplateSyntaxError(
53+
"querystring requires mappings for positional arguments (got "
54+
"%r instead)." % d
55+
)
56+
for key, value in d.items():
57+
if not isinstance(key, str):
58+
raise TemplateSyntaxError(
59+
"querystring requires strings for mapping keys (got %r "
60+
"instead)." % key
61+
)
62+
if value is None:
63+
params.pop(key, None)
64+
elif isinstance(value, Iterable) and not isinstance(value, str):
65+
params.setlist(key, value)
66+
else:
67+
params[key] = value
68+
query_string = params.urlencode() if params else ""
69+
return f"?{query_string}"

base/templatetags/components.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from django import template
2+
from django.utils.html import format_html
3+
from django.utils.safestring import mark_safe
4+
5+
from base.pagination import PAGE_VAR
6+
from .base_templatetags import querystring
7+
8+
register = template.Library()
9+
10+
11+
@register.simple_tag
12+
def pagination_number(pagination, i):
13+
"""
14+
Generate an individual page index link in a paginated list.
15+
"""
16+
if i == pagination.paginator.ELLIPSIS:
17+
return format_html("{} ", pagination.paginator.ELLIPSIS)
18+
elif i == pagination.page_num:
19+
return format_html('<em class="current-page" aria-current="page">{}</em> ', i)
20+
else:
21+
link = querystring(None, pagination.params, {PAGE_VAR: i})
22+
return format_html(
23+
'<a href="{}" aria-label="page {}" {}>{}</a> ',
24+
link,
25+
i,
26+
mark_safe(' class="end"' if i == pagination.paginator.num_pages else ""),
27+
i,
28+
)
29+
30+
31+
@register.inclusion_tag("base/components/pagination.html", name="pagination")
32+
def pagination_tag(pagination):
33+
previous_page_link = f"?{PAGE_VAR}={pagination.page_num - 1}"
34+
next_page_link = f"?{PAGE_VAR}={pagination.page_num + 1}"
35+
return {
36+
"pagination": pagination,
37+
"previous_page_link": previous_page_link,
38+
"next_page_link": next_page_link,
39+
}

base/tests/__init__.py

Whitespace-only changes.

base/tests/migrations/0002_fish.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Generated by Django 3.2.15 on 2025-06-04 05:19
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('tests', '0001_initial'),
10+
]
11+
12+
operations = [
13+
migrations.CreateModel(
14+
name='Fish',
15+
fields=[
16+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
17+
('name', models.CharField(max_length=255)),
18+
('price', models.IntegerField()),
19+
],
20+
),
21+
]

base/tests/migrations/__init__.py

Whitespace-only changes.

ratings/tests/models.py renamed to base/tests/models.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
from django.db import models
22

3-
from ..models import RatedItemBase, Ratings
3+
from ratings.models import RatedItemBase, Ratings
4+
5+
6+
class Fish(models.Model):
7+
name = models.CharField(max_length=255)
8+
price = models.IntegerField()
49

510

611
class Food(models.Model):

base/tests/tests.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from django.test import RequestFactory, TestCase
2+
from base.pagination import Pagination
3+
4+
from .models import Fish
5+
6+
7+
class PaginationTestCase(TestCase):
8+
@classmethod
9+
def setUpTestData(cls):
10+
fishs = [Fish(name=f"fish-{i}", price=i * 100) for i in range(1, 101)]
11+
Fish.objects.bulk_create(fishs)
12+
cls.queryset = Fish.objects.all()
13+
cls.factory = RequestFactory()
14+
15+
def test_pagination_attributes(self):
16+
request = self.factory.get("/fake-url/")
17+
pagination = Pagination(request, Fish, self.queryset, 5)
18+
self.assertEqual(pagination.result_count, 100)
19+
self.assertTrue(pagination.multi_page)
20+
pagination = Pagination(request, Fish, self.queryset, 200)
21+
self.assertFalse(pagination.multi_page)
22+
23+
def test_pagination_page_range(self):
24+
request = self.factory.get("/fake-url/")
25+
ELLIPSIS = "…"
26+
case = [
27+
(2, 6, [1, 2, 3, 4, 5, 6, 7, 8, 9, ELLIPSIS, 49, 50]),
28+
(3, 10, [1, 2, ELLIPSIS, 7, 8, 9, 10, 11, 12, 13, ELLIPSIS, 33, 34]),
29+
(4, 23, [1, 2, ELLIPSIS, 20, 21, 22, 23, 24, 25]),
30+
(5, 20, [1, 2, ELLIPSIS, 17, 18, 19, 20]),
31+
(10, 8, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
32+
(20, 1, [1, 2, 3, 4, 5]),
33+
]
34+
for list_per_page, current_page, expected_page_range in case:
35+
with self.subTest(list_per_page=list_per_page, current_page=current_page):
36+
pagination = Pagination(request, Fish, self.queryset, list_per_page)
37+
pagination.page_num = current_page
38+
self.assertEqual(list(pagination.page_range), expected_page_range)
39+
40+
def test_pagination_result_objects(self):
41+
request = self.factory.get("/fake-url/")
42+
case = [
43+
(2, 25, ["49", "50"]),
44+
(4, 12, ["45", "46", "47", "48"]),
45+
(5, 10, ["46", "47", "48", "49", "50"]),
46+
(7, 11, ["71", "72", "73", "74", "75", "76", "77"]),
47+
(10, 10, ["91", "92", "93", "94", "95", "96", "97", "98", "99", "100"]),
48+
(200, 1, [str(i) for i in range(1, 101)]),
49+
]
50+
Fish.objects.all().delete()
51+
fishs = [Fish(name=i, price=i * 100) for i in range(1, 101)]
52+
Fish.objects.bulk_create(fishs)
53+
queryset = Fish.objects.all().order_by("id")
54+
for list_per_page, current_page, expect_object_names in case:
55+
pagination = Pagination(request, Fish, queryset, list_per_page)
56+
pagination.page_num = current_page
57+
objects = pagination.get_objects()
58+
object_names = list(objects.values_list("name", flat=True))
59+
self.assertEqual(object_names, expect_object_names)

0 commit comments

Comments
 (0)