在 使用 Django Pagination 实现简单的分页功能 中,我们实现了一个简单的分页导航效果。但想实现下面这样的一个比较完善的分页导航时,Django Pagination 内置的 API 已经无法满足需求。本文将通过拓展 Django Pagination 来实现下图这样比较完善的分页效果。
分页效果概述
一个比较完善的分页效果应该具有以下特性,就像上图展示的那样,很多网站都采用了类似这种的分页导航方式。
- 始终显示第一页和最后一页
- 当前页码高亮显示
- 显示当前页码前后几个连续的页码
- 如果两个页码号间还有其它页码,中间显示省略号以提示用户
类视图 ListView
由于在开发网站的过程中,有一些视图函数虽然处理的对象不同,但是其大致的代码逻辑是一样的。比如一个博客和一个论坛,通常其首页都是展示一系列的文章列表或者帖子列表。对处理首页的视图函数来说,虽然其处理的对象一个是文章,另一个是帖子,但是其处理的过程是非常类似的。首先是从数据库取出文章或者帖子列表,然后将这些数据传递给模板并渲染模板。
于是 Django 把这些相同的逻辑代码抽取了出来,写成了一系列的通用视图函数,即基于类的通用视图。本文将使用到通用视图 ListView。ListView 用来从数据库获取一个对象列表,而对列表进行分页的过程也是比较通用的,ListView 已经实现了分页功能。所以我们直接使用 ListView 而不是自己写分页逻辑,以达到代码复用的目的。
ListView 的使用非常简单,只需要将你自己的视图继承 ListView ,然后复写一些属性和方法即可。例如我们博客的首页视图 index
代码如下:
blog/views.py
def index(request):
post_list = Post.objects.all()
paginator = Paginator(post_list, 1)
page = request.GET.get('page')
try:
post_list = paginator.page(page)
except PageNotAnInteger:
post_list = paginator.page(1)
except EmptyPage:
post_list = paginator.page(paginator.num_pages)
return render(request, 'blog/index.html', context={'post_list': post_list})
现在将其转化为等价的类视图如下:
blog/views.py
class IndexView(ListView):
model = Post
template_name = 'blog/index.html'
context_object_name = 'post_list'
paginate_by = 10
指定 model
属性的值后,Django 就会根据指定的模型去数据库获取该模型的列表。
template_name
指定要渲染的模板文件。
context_object_name
指定模型列表数据传递给模板的变量名。
paginate_by
指定对获取到的模型列表进行分页,这里每页 10 个数据。
URL 的配置一开始是这样的:
blog/urls.py
app_name = 'blog'
urlpatterns = [
url(r'^$', views.index, name='index'),
...
]
url
函数接收的一个参数是一个正则表达式,用于匹配用户请求的 URL 模式。第二个参数是被调用的视图函数,其类型必须是一个函数。而我们写的 IndexView
视图是一个类,为了将其转换成一个函数,只需要调用其父类中的 as_view
方法即可。因此将 URL 的配置改为:
blog/urls.py
app_name = 'blog'
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
...
]
ListView 分页后会给模板传递一个
is_paginated
和一个 page
变量。前者用于标示是否分页,后者是一个 Page
对象。因此在模板中设置一个简单的分页导航如下:
{% if is_paginated %}
<div class="pagination">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">上一页</a>
{% endif %}
<span class="current">
第 {{ page_obj.number }} 页 / 共 {{ page_obj.paginator.num_pages }} 页
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">下一页</a>
{% endif %}
</div>
{% endif %}
此时的分页效果和 使用 Django Pagination 实现简单的分页功能 实现的效果是一样的了。
拓展 Pagination
为了实现如下所展示的分页效果,接下来就需要在 ListView 的基础上进一步拓展分页的逻辑代码。
可以看到整个分页导航条其实可以分成 七个部分:
- 第 1 页页码,这一页需要始终显示。
- 第 1 页页码后面的省略号部分。但要注意只有如果第一页的页码号后面紧跟着页码号 2,那么省略号就不应该显示。
- 当前页码的左边部分,比如这里的 3-6。
- 当前页码。
- 当前页码的右边部分,比如这里的 8-11。
- 最后一页页码前面的省略号部分。但要注意如果最后一页的页码号前面跟着的页码号是连续的,那么省略号就不应该显示。
- 最后一页的页码号。
因此我们的思路是,在视图里将以上七步中所需要的数据生成,然后传递给模板在模板中渲染显示就行。整个视图的代码如下,代码实现的功能已有详细注释,就不在文章中进一步说明了。
class IndexView(ListView):
model = Post
template_name = 'blog/index.html'
context_object_name = 'post_list'
paginate_by = 10
def get_context_data(self, **kwargs):
"""
在视图函数中将模板变量传递给模板是通过给 render 函数传递一个字典实现的
例如 render(request, 'blog/index.html', context={'post_list': post_list})
这里传递了一个 {'post_list': post_list} 字典给模板。
在类视图中,这个需要传递的模板变量字典是通过 get_context_data 获得的,
所以我们复写该方法,以便我们能够自己再插入一些我们自定义的模板变量进去。
"""
# 首先获得父类生成的传递给模板的字典
context = super().get_context_data(**kwargs)
# 父类生成的字典中已有 paginator、page_obj、is_paginated 这三个模板变量
# paginator 是 Paginator 的一个实例
# page_obj 是 Page 的一个实例
# is_paginated 是一个布尔变量,用于指示是否已分页。
# 例如如果规定每页 10 个数据,而本身只有 5 个数据,其实就用不着分页,此时 is_paginated=False。
# 关于什么是 Paginator,Page 类在 使用 Django Pagination 实现简单的分页功能:http://zmrenwu.com/post/23/
# 中已有详细说明。
paginator = context.get('paginator')
page = context.get('page_obj')
is_paginated = context.get('is_paginated')
# 调用自己写的 pagination_data 方法获得显示分页导航条需要的数据
pagination_data = self.pagination_data(paginator, page, is_paginated)
# 将分页导航条的模板变量更新到 context 中
context.update(pagination_data)
# 将更新后的 context 返回,以便 ListView 使用这个字典中的模板变量去渲染模板
# 记住此时字典中已有了显示分页导航条所需的数据
return context
def pagination_data(self, paginator, page, is_paginated):
if not is_paginated:
# 如果没有分页,则无需显示分页导航条,不用任何分页导航条的数据,因此返回一个空的字典
return {}
# 当前页左边连续的页码号,初始值为空
left = []
# 当前页右边连续的页码号,初始值为空
right = []
# 标示第一页页码后是否需要显示省略号
left_has_more = False
# 标示最后一页页码前是否需要显示省略号
right_has_more = False
# 标示是否需要显示第一页的页码号。
# 因为如果当前页左边的连续页码号中已经含有第一页的页码号,此时就无需再显示第一页的页码号
# 其它情况下第一页的页码是始终需要显示的。
first = False
# 标示是否需要显示最后一页的页码号。
# 需要此指示变量的理由和上面相同。
last = False
# 获得用户当前请求的页码号
page_number = page.number
# 获得分页后的总页数
total_pages = paginator.num_pages
# 获得整个分页页码列表,比如分了四页,那么就是 [1, 2, 3, 4]
page_range = paginator.page_range
if page_number == 1:
# 如果用户请求的是第一页的数据,那么当前页左边的不需要数据,因此 left=[](已默认为空)
# 获取当前页右边的连续页码号。
# 比如分页页码列表是 [1, 2, 3, 4],那么获取的就是 right = [2, 3]
# 这里只获取了当前页码后连续两个页码,你可以更改这个数字以获取更多页码。
right = page_range[page_number:page_number + 2]
# 如果最右边的页码号比最后一页的页码号减去 1 还要小,
# 说明最右边的页码号和最后一页的页码号之间还有其它页码,因此需要显示省略号,通过 right_has_more 来指示
if right[-1] < total_pages - 1:
right_has_more = True
# 如果最右边的页码号比最后一页的页码号小,说明当前页右边的连续页码号中不包含最后一页的页码
# 所以需要显示最后一页的页码号,通过 last 来指示
if right[-1] < total_pages:
last = True
elif page_number == total_pages:
# 如果用户请求的是最后一页的数据,那么当前页右边就不需要数据,因此 right=[](已默认为空)
# 获取当前页左边的连续页码号。
# 比如分页页码列表是 [1, 2, 3, 4],那么获取的就是 left = [2, 3]
# 这里只获取了当前页码后连续两个页码,你可以更改这个数字以获取更多页码。
left = page_range[(page_number - 3) if (page_number - 3) > 0 else 0:page_number - 1]
# 如果最左边的页码号比第 2 页页码号还大,
# 说明最左边的页码号和第一页的页码号之间还有其它页码,因此需要显示省略号,通过 left_has_more 来指示
if left[0] > 2:
left_has_more = True
# 如果最左边的页码号比第一页的页码号大,说明当前页左边的连续页码号中不包含第一页的页码
# 所以需要显示第一页的页码号,通过 first 来指示
if left[0] > 1:
first = True
else:
# 用户请求的既不是最后一页,也不是第一页,则需要获取当前页左右两边的连续页码号
# 这里只获取了当前页码前后连续两个页码,你可以更改这个数字以获取更多页码。
left = page_range[(page_number - 3) if (page_number - 3) > 0 else 0:page_number - 1]
right = page_range[page_number:page_number + 2]
# 是否需要显示最后一页和最后一页前的省略号
if right[-1] < total_pages - 1:
right_has_more = True
if right[-1] < total_pages:
last = True
# 是否需要显示第一页和第一页后的省略号
if left[0] > 2:
left_has_more = True
if left[0] > 1:
first = True
context = {
'left': left,
'right': right,
'left_has_more': left_has_more,
'right_has_more': right_has_more,
'first': first,
'last': last,
}
return context
模板中设置分页导航
接下来便是在模板中设置分页导航了,将导航条的七个部分一一展现即可,示例代码如下:
{% if is_paginated %}
<div class="pagination">
{% if first %}
<a href="?page=1">1</a>
{% endif %}
{% if left %}
{% if left_has_more %}
<span>...</span>
{% endif %}
{% for i in left %}
<a href="?page={{ i }}">{{ i }}</a>
{% endfor %}
{% endif %}
<a href="?page={{ page_obj.number }}" style="color: red">{{ page_obj.number }}</a>
{% if right %}
{% for i in right %}
<a href="?page={{ i }}">{{ i }}</a>
{% endfor %}
{% if right_has_more %}
<span>...</span>
{% endif %}
{% endif %}
{% if last %}
<a href="?page={{ paginator.num_pages }}">{{ paginator.num_pages }}</a>
{% endif %}
</div>
{% endif %}
在示例项目中的效果如下:
要使分页导航更加美观,通过设置其 css 样式即可。
-- EOF --
仅供Py2的使用者参考
super()部分应为
context = super(IndexView, self).get_context_data(**kwargs)
博主使用的Python3,和Py2中range用法有所不同,
后面因Py2中xrange不同在于:paginator.page_range返回的xrange对象只接受[x]这样的索引,不能使用切片返回list。但指定索引时page.number 返回从1开始,实际页面数,而对应的为xrange[0],如果单纯的指定page.number = page.number - 1后面逻辑要动的很多= =。我只是在index out of range时指定右边界,使用try捕获异常。。下面为实际改动部分,仅供参考~
!注:为了测试分布效果,设置paginate_by = 1,具体设置为别的还需再做调整
super() takes at least 1 argument (0 given)
这是为什么啊
Python2 请使用 python2 的super 函数写法,具体请百度或者查看 py2文档。
博主确实厉害,学习了,今天刚看了一下paginator,能实现基本功能,但是像博主这样的功能没什么思路,看了一下博主的想法,很有借鉴性