Skip to content

Commit 17f37ab

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

21 files changed

+299
-135
lines changed

base/__init__.py

Whitespace-only 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+
}

cab/utils.py

Lines changed: 21 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22

33
import bleach
44
from django.core.exceptions import ObjectDoesNotExist
5-
from django.core.paginator import InvalidPage, Paginator
65
from django.http import Http404, HttpResponse
76
from django.template import loader
87
from django.utils.safestring import mark_safe
98
from markdown import markdown as markdown_func
109

10+
from base.pagination import Pagination
11+
1112

1213
def object_list(
1314
request,
@@ -28,101 +29,43 @@ def object_list(
2829
Context:
2930
object_list
3031
list of objects
31-
is_paginated
32-
are the results paginated?
33-
results_per_page
34-
number of objects per page (if paginated)
35-
has_next
36-
is there a next page?
37-
has_previous
38-
is there a prev page?
39-
page
40-
the current page
41-
next
42-
the next page
43-
previous
44-
the previous page
45-
pages
46-
number of pages, total
32+
pagination
33+
This is a pagination object that holds attributes
34+
related to pagination.
35+
For more detail, please refer to the `base.pagination.Pagination` class.
4736
hits
4837
number of objects, total
49-
last_on_page
50-
the result number of the last of object in the
51-
object_list (1-indexed)
52-
first_on_page
53-
the result number of the first object in the
54-
object_list (1-indexed)
55-
page_range:
56-
A list of the page numbers (1-indexed).
5738
"""
5839
if extra_context is None:
5940
extra_context = {}
6041
queryset = queryset._clone()
42+
model = queryset.model
43+
opts = model._meta
6144
if paginate_by:
62-
paginator = Paginator(queryset, paginate_by, allow_empty_first_page=allow_empty)
63-
if not page:
64-
page = request.GET.get("page", 1)
45+
pagination = Pagination(request, model, queryset, paginate_by)
46+
object_list = pagination.get_objects()
6547

66-
if page == "last":
67-
page_number = paginator.num_pages
68-
else:
69-
try:
70-
page_number = int(page)
71-
except ValueError:
72-
# Page is not 'last', nor can it be converted to an int.
73-
raise Http404
74-
try:
75-
page_obj = paginator.page(page_number)
76-
except InvalidPage:
77-
raise Http404
78-
try:
79-
next_page = page_obj.next_page_number()
80-
except InvalidPage:
81-
next_page = None
82-
try:
83-
previous_page = page_obj.previous_page_number()
84-
except InvalidPage:
85-
previous_page = None
86-
87-
c = {
88-
"%s_list" % template_object_name: page_obj.object_list,
89-
"paginator": paginator,
90-
"page_obj": page_obj,
91-
"is_paginated": page_obj.has_other_pages(),
92-
# Legacy template context stuff. New templates should use page_obj
93-
# to access this instead.
94-
"results_per_page": paginator.per_page,
95-
"has_next": page_obj.has_next(),
96-
"has_previous": page_obj.has_previous(),
97-
"page": page_obj.number,
98-
"next": next_page,
99-
"previous": previous_page,
100-
"first_on_page": page_obj.start_index(),
101-
"last_on_page": page_obj.end_index(),
102-
"pages": paginator.num_pages,
103-
"hits": paginator.count,
104-
"page_range": paginator.page_range,
48+
context = {
49+
"%s_list" % template_object_name: object_list,
50+
"pagination": pagination,
51+
"hits": pagination.result_count,
10552
}
10653
else:
107-
c = {
108-
"%s_list" % template_object_name: queryset,
109-
"paginator": None,
110-
"page_obj": None,
111-
"is_paginated": False,
54+
context = {
55+
"%s_list" % template_object_name: object_list,
11256
}
11357
if not allow_empty and len(queryset) == 0:
11458
raise Http404
11559

11660
for key, value in extra_context.items():
11761
if callable(value):
118-
c[key] = value()
62+
context[key] = value()
11963
else:
120-
c[key] = value
64+
context[key] = value
12165
if not template_name:
122-
model = queryset.model
123-
template_name = "%s/%s_list.html" % (model._meta.app_label, model._meta.object_name.lower())
124-
t = template_loader.get_template(template_name)
125-
return HttpResponse(t.render(c, request=request), content_type=content_type)
66+
template_name = "%s/%s_list.html" % (opts.app_label, opts.object_name.lower())
67+
template = template_loader.get_template(template_name)
68+
return HttpResponse(template.render(context, request=request), content_type=content_type)
12669

12770

12871
def object_detail(

djangosnippets/settings/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def user_url(user):
5656
"allauth.socialaccount.providers.bitbucket",
5757
"allauth.socialaccount.providers.github",
5858
"allauth.socialaccount.providers.twitter",
59+
"base",
5960
"cab",
6061
"comments_spamfighter",
6162
"ratings",

djangosnippets/static/scss/main.scss

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ body.with-sidebar {
317317
@include grid-column(4);
318318
}
319319
}
320-
.pagination, .count {
320+
.count {
321321
text-align: center;
322322
}
323323
}
@@ -358,6 +358,66 @@ body.simple {
358358
}
359359
}
360360

361+
nav.pagination {
362+
display: flex;
363+
justify-content: center;
364+
text-align: center;
365+
ul {
366+
margin-left: 1rem;
367+
margin-right: 1rem;
368+
}
369+
370+
li {
371+
display: inline-block;
372+
a, em, span {
373+
padding: 5px 10px;
374+
min-width: 32px;
375+
line-height: 20px;
376+
border: 1px solid transparent;
377+
border-radius: 6px;
378+
transition: border-color .2s cubic-bezier(0.3, 0, 0.5, 1);
379+
cursor: pointer;
380+
}
381+
a:hover {
382+
border-color: $secondary-color;
383+
text-decoration: none;
384+
}
385+
em {
386+
font-style: normal;
387+
cursor: default;
388+
}
389+
.current-page {
390+
font-weight: bold;
391+
color: white;
392+
background-color: $secondary-color;
393+
}
394+
.disabled {
395+
color: gray;
396+
cursor: default;
397+
border-color: transparent;
398+
}
399+
}
400+
401+
.previous-page::before, .next-page::after {
402+
display: inline-block;
403+
width: 1rem;
404+
height: 1rem;
405+
vertical-align: text-bottom;
406+
content: "";
407+
background-color: currentColor;
408+
}
409+
410+
.previous-page::before {
411+
clip-path: polygon(9.8px 12.8px, 8.7px 12.8px, 4.5px 8.5px, 4.5px 7.5px, 8.7px 3.2px, 9.8px 4.3px, 6.1px 8px, 9.8px 11.7px, 9.8px 12.8px);
412+
margin-right: 4px;
413+
}
414+
415+
.next-page::after {
416+
clip-path: polygon(6.2px 3.2px, 7.3px 3.2px, 11.5px 7.5px, 11.5px 8.5px, 7.3px 12.8px, 6.2px 11.7px, 9.9px 8px, 6.2px 4.3px, 6.2px 3.2px);
417+
margin-left: 4px;
418+
}
419+
}
420+
361421
footer {
362422
padding: 30px 0 30px 0;
363423
clear: both;

0 commit comments

Comments
 (0)