在 Django Pagination 简单分页 中,我们实现了一个简单的分页导航效果。但效果有点差强人意,我们只能点上一页和下一页的按钮进行翻页。比较完善的分页效果应该像下面这样,但想实现这样一种效果,Django Pagination 内置的 API 已无能为力。本文将通过拓展 Django Pagination 来实现下图这样比较完善的分页效果。
分页效果概述
一个比较完善的分页效果应该具有以下特性,就像上图展示的那样,很多网站都采用了类似这种的分页导航方式。
- 始终显示第一页和最后一页。
- 当前页码高亮显示。
- 显示当前页码前后几个连续的页码。
- 如果两个页码号间还有其它页码,中间显示省略号以提示用户。
拓展 Pagination
在此之前,我们已将首页文章列表的视图函数转为了类视图,并且使用了类视图 ListView
中已经为我们写好的分页代码来达到分页的目的(详情请查看文章开头处给出的链接)。为了实现如下所展示的分页效果,接下来就需要在 ListView
的基础上进一步拓展分页的逻辑代码。
先来分析一下导航条的组成部分,可以看到整个分页导航条其实可以分成 七个部分:
- 第 1 页页码,这一页需要始终显示。
- 第 1 页页码后面的省略号部分。但要注意如果第 1 页的页码号后面紧跟着页码号 2,那么省略号就不应该显示。
- 当前页码的左边部分,比如这里的 3-6。
- 当前页码,比如这里的 7。
- 当前页码的右边部分,比如这里的 8-11。
- 最后一页页码前面的省略号部分。但要注意如果最后一页的页码号前面跟着的页码号是连续的,那么省略号就不应该显示。
- 最后一页的页码号。
因此我们的思路是,在视图里将以上七步中所需要的数据生成,然后传递给模板并在模板中渲染显示即可。整个视图的代码如下,由于代码比较长,所以代码实现的功能直接在代码块中注释,就不在文章中进一步说明了。推荐使用大屏幕阅读器获取更好的阅读体验。
blog/views.py
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 函数的 context 参数传递一个字典实现的,
例如 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/34/ 中已有详细说明。
# 由于 context 是一个字典,所以调用 get 方法从中取出某个键对应的值。
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 中,注意 pagination_data 方法返回的也是一个字典。
context.update(pagination_data)
# 将更新后的 context 返回,以便 ListView 使用这个字典中的模板变量去渲染模板。
# 注意此时 context 字典中已有了显示分页导航条所需的数据。
return context
def pagination_data(self, paginator, page, is_paginated):
if not is_paginated:
# 如果没有分页,则无需显示分页导航条,不用任何分页导航条的数据,因此返回一个空的字典
return {}
# 当前页左边连续的页码号,初始值为空
left = []
# 当前页右边连续的页码号,初始值为空
right = []
# 标示第 1 页页码后是否需要显示省略号
left_has_more = False
# 标示最后一页页码前是否需要显示省略号
right_has_more = False
# 标示是否需要显示第 1 页的页码号。
# 因为如果当前页左边的连续页码号中已经含有第 1 页的页码号,此时就无需再显示第 1 页的页码号,
# 其它情况下第一页的页码是始终需要显示的。
# 初始值为 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 页页码号还大,
# 说明最左边的页码号和第 1 页的页码号之间还有其它页码,因此需要显示省略号,通过 left_has_more 来指示。
if left[0] > 2:
left_has_more = True
# 如果最左边的页码号比第 1 页的页码号大,说明当前页左边的连续页码号中不包含第一页的页码,
# 所以需要显示第一页的页码号,通过 first 来指示
if left[0] > 1:
first = True
else:
# 用户请求的既不是最后一页,也不是第 1 页,则需要获取当前页左右两边的连续页码号,
# 这里只获取了当前页码前后连续两个页码,你可以更改这个数字以获取更多页码。
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
# 是否需要显示第 1 页和第 1 页后的省略号
if left[0] > 2:
left_has_more = True
if left[0] > 1:
first = True
data = {
'left': left,
'right': right,
'left_has_more': left_has_more,
'right_has_more': right_has_more,
'first': first,
'last': last,
}
return data
模板中设置分页导航
接下来便是在模板中设置分页导航了,将导航条的七个部分的数据一一展现即可,示例代码如下:
{% 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 样式即可:
总结
本章节的代码位于:Step20: complete pagination。
如果遇到问题,请通过下面的方式寻求帮助。
- 简单问题在下方评论区留言。
- 在 Pythonzhcn 社区的新手问答版块 发布帖子。
- 在 Pythonzhcn 社区的新手问答版块 发布帖子。
-- EOF --
博主你好!
我想问下,我照着代码一路写下来,但是在index完善分页这里,不知道为什么,感觉left,right等等值并没有传入index中,导致无法显示分页排版,请问这是什么原因呢?
调试了下,出来了,谢谢
博主您好。
非常感谢您的教程。有个问题想咨询您一下,一开始我通过您的教程能够成功实现刷新页面分页,然后我通过jQuery的load功能加载对应页面的文章列表的div,实现了不刷新页面分页。但问题来了,我的分页菜单没有更新,导致分页菜单一直都是1,2,3,...,7,8这种形式,4-6页的内容无法通过点击分页菜单的页码加载。然后我试了下分页div刷新时,同时也将分页菜单的div替换成新页面中对应的分页菜单,但问题又来了,替换后的分页菜单只能点击一次,再次点击就没有相应了,也不清楚是什么问题。想请教一下博主有没有方法可供参考一下?我的view和html里面的代码跟您给的一样,jQuery代码如下:
<script>
$('#p_manu_num a').on('click', function () { //定义分页菜单中a元素的功能
var art_url = "?page=" + $(this).attr('name') +" #blog_content_arts" //获取目标页面的url中的文章列表
var manu_url = "?page=" + $(this).attr('name') +" #p_manu_num" //获取目标页面的url中的分页菜单
$('#blog_content_arts').load(art_url).transition('fade in') //加载目标页面中的文章列表到对应div中
$('#p_manu_num').load(manu_url) //加载目标页面中的分页菜单到对应div中
})
</script>
或者,django能否结合ajax实现异步分页功能呢?
望博主不吝赐教,十分感谢!
抱歉,我忘了,我html中的代码跟您略有不同,如下所示:
请问,为什么访问前三页分页没有问题,后面的就无法访问了。
TypeError at /
'bool' object is not iterable
Request Method:GET
Request URL:http://localhost:8000/?page=4
Django Version:2.1.3
Exception Type:TypeError
Exception Value:'bool' object is not iterable
Exception Location:G:\Anaconda3\envs\web\lib\site-packages\django\template\defaulttags.py in render, line 165Python Executable:G:\Anaconda3\envs\web\python.exe
Python Version:3.6.2
Python Path:['D:\\PycharmProjects\\web\\blogproject', 'G:\\Anaconda3\\envs\\web\\python36.zip', 'G:\\Anaconda3\\envs\\web\\DLLs', 'G:\\Anaconda3\\envs\\web\\lib', 'G:\\Anaconda3\\envs\\web', 'G:\\Anaconda3\\envs\\web\\lib\\site-packages']
Server time:星期三, 19 十二月 2018 09:09:01 +0800
Error during template rendering
In template D:\PycharmProjects\web\blogproject\templates\base.html, error at line 0
'bool' object is not iterable
1<!DOCTYPE html>
2<!--模板标签(具有类似函数的功能)-->
3{% load staticfiles %}
4{% load blog_tags %}
5<html>
6<head>
7 <title>BigOrange & Blog</title>
8 <style>
9 span.highlighted{
10 color: #00CCFF;
解决了,下面评论的简洁代码有bug。
请问你怎么解决的哈,我遇到了跟你一样的问题。我用的是博主的代码,不是下面评论的代码。
CSS可以了,感谢~
博主您好,请问如果我没有使用您的通用类视图,要如何写高级的分页功能呢? 请问能给个思路或案例嘛?非常感谢您~~~ :)
哈哈 我搞定啦! 谢谢楼主啦~~ 看到文章内那么多代码心里害怕了,但是其实仔细读一读是可以读懂的
楼主您好!请问分页的居中如何设置呢?我在div中使用 align="center"但是没有效果。谢谢!
博主,还想没有实现当前页的页码颜色加深哦。。。。
参考评论区的代码,自行添加 css 即可。
测试样例
sequence index must be integer, not ‘slice’
这是因为xrange对象不能进行slice操作,进入templatetags,将pagination_tags.py,paginate函数里的page_range = paginator.page_range改为 page_range = list(paginator.page_range)
老师的思路非常好,我来顺便简化一下一些重复的代码.
那这样看起来只有index主页有分页啊进入标签页,类别页会调用index view中的分页吗?
分类和归档用之前那的视图类继承
class ArchivesView(IndexView)
感谢楼主的教程~
贴一下IndexView里的paginator_data函数的比较简洁的写法:
NICE 很好使
分享一段css代码用于美化分页,效果类似于开头的示例:
还需要改写模板中的一行代码:
改为:
可以用,谢谢
大哥,这段css代码,贴在哪个位置,原谅小弟不才,问这么简单的问题。
大哥,这段css代码,贴在哪个位置,原谅小弟不才,问这么简单的问题。
自定义style
骚气紫色高亮
感谢啊,这个颜色适合我,骚气紫!
您好!请问分页的居中如何设置呢?我在div中使用 align="center"但是没有效果。谢谢!
在<div class="pagination">外面加一层:<div class="text-center">
越俎代庖,回答一下;可以复制到custom.css的后面;浏览器要清除缓存,刷新,就能看到效果了
博主,在调用父类的方法的时候,super是不是需要添加一下参数?
context = super().get_context_data(**kwargs)
改为 context = super(IndexView,self).get_context_data(**kwargs)
不然的话 可能出现index界面无法读取文章的情况,不知道是不是python或者django环境版本的问题。这是我测试的时候出现的一个bug
好像不是这个的问题,我又出现了这个bug 尴尬
这是 python3 的写法,python2需要使用父类和self作为参数。
出现了一个问题,假如说是总共两页的时候,按照您的代码,浏览第二页的时候,页码1是不显示的,我觉得应该修改一下
这个地方,如果是总共两页的话,left应该是left=page_range[0:1],此时page_range=[1,2],即left=[1],这个时候如果去比较left[0]与1的大小的话,first=False了,我觉得应该修改此处的代码。不知道我的逻辑对不对
可能吧,但我当时测试的时候好像没有发现问题,你测试一下应该就知道了。有问题的话修改一下逻辑即可。
谢谢博主,抽出宝贵的上班时间回复消息
博主你好,完善页码之后出现如下错误是为什么呢?
context = super().get_context_data(**kwargs)
你是Python2还是3?
我该环境之后没问题了,谢谢博主!
不用客气,我不是博主^_^
博主好, blog/views中添加了分页后, 执行127.0.0.1:8000显示以下错误;
TypeError at /'NoneType' object is not iterable
具体错误如下, 表中有13条数据 , 设置了paginate_by = 2, 不明白为什么pagination_data = None ? 谢谢指点
/blogproject/blog/views.py in get_context_data
28. context.update(pagination_data)
Local Vars
Variable Value
__class__ <class 'blog.views.IndexView'>
context {'is_paginated': True, 'object_list': <QuerySet [<Post: r234223>, <Post: kjjadf>]>, 'page_obj': <Page 1 of 7>, 'paginator': <django.core.paginator.Paginator object at 0x1097d53c8>, 'post_list': <QuerySet [<Post: r234223>, <Post: kjjadf>]>, 'view': <blog.views.IndexView object at 0x1097d5358>}
is_paginated True
kwargs {}
page <Page 1 of 7>
pagination_data None
paginator <django.core.paginator.Paginator object at 0x1097d53c8>
self <blog.views.IndexView object at 0x1097d5358
这通常是你的 url 写错了,对比一下文章中的,仔细一点。
Error during template rendering
In template D:\Django\blogproject\templates\base.html, error at line 15[0-9]+)/$']Black & White
Reverse for 'category' with arguments '('',)' not found. 1 pattern(s) tried: ['category/(?P
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
博主报了这个错误,这是哪错了?
这通常是你的 url 写错了,对比一下文章中的,仔细一点。
把+改成*就对了==
首页类视图
class IndexView(ListView):
model = Article
template_name = 'blog/index.html'
context_object_name = 'art_list'
ordering = '-art_ctime'
paginate_by = 1
这是我写的,有些地方和博主的不一样,是自定义的,测试如果有问题的可以参考。有些地方分开可能好理解一点。
data 字典那里复制的时候少了个 }
我感觉作者写的代码逻辑有问题,但是运行却能完美运行,这是DJango里面自带的功能吗?比如
if page_number == 1:
right = page_range[page_number:page_number + 2]
当页面只有2页的话,最后right的值却是2和3 但是page_range是没有这个值的
还有比如:
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]
当请求到倒数第二页时,right的最大那个比倒数第一页的值还大1
但还是没报错
看来是我对切片理解不够深了,试了一下,超出范围的切片后,python止取出有效的数值
博主,我现在遇到一个问题是:在admin中添加post文章时,在选择tag和category时他们给出的选项是tag object和category object,但是我想要的是tag或者category里边的(Java,Python,数据结构......)等,试了几个方法还是不行
你需要为 model 定义 str 方法,详情见之前关于model 部分的教程。
博主,我没用类方法写视图,没有继承ListView,没有page_obj、is_paginated,这两个字典。
可以参考着上一篇的内容自己创建,明白了运行原理自己写也不难。
博主!我问个问题,我按照你的博文代码一步一步理解敲的,也和GitHub上的代码核对过了,并无出入,这么显示不了上一页和下一页的按钮呢
是不是测试地数据太少不足以分页呀?
亲测我觉得博主的代码没问题
只是我用的py2.7 page_range返回的是一个迭代器,要用list转化一下就好。
至于right[page_num:page_num+2] 和left[]的切片,顶多为空(当前页是最后一页),但是即使超出列表的index也不会报错。这样写right/left列表的长度可能为0,可能为1,也可能为2。比较灵活。
感谢博主能提供这么好的教程!
使用python2.7的同学super().get_context_data(kwargs)这里会报错,应该要改下super(IndexView, self).get_context_data(kwargs)。
使用2.7还需要改写的地方是paginator.page_range,会得到一个xrange的生成器,这个xrange是不能切片的,但是3.6版本没有xrange了,xrange已经变成range,所以可以切片
嗯,对 py2 的兼容我没有考虑了。
没切片,用list(page_range)就好勒
首先感谢博主精彩的教程,目前我有一个问题,在将index视图函数用类改写以后,访问主页(index.html)却得到这样的错误:TemplateDoesNotExist: blogs/post_list.html,(blogs是我的app名称),虽然我使用的是py2,但我已经按上面其他同学给出的方法仔细地重写了有关super方法处和page_range处的代码,但仍然得到这个错误,模板文件的位置也已经做了检查,奇怪的是,用同样方法复写的Detail ,archives和category等视图函数均可以访问成功,在此请教博主和各位同学,希望能够得到解答,多谢!完整的报错信息如下:
Traceback (most recent call last):
File "/home/zvector/BlogLearn/venv/local/lib/python2.7/site-packages/django/core/handlers/exception.py", line 41, in inner
response = get_response(request)
File "/home/zvector/BlogLearn/venv/local/lib/python2.7/site-packages/django/core/handlers/base.py", line 217, in _get_response
response = self.process_exception_by_middleware(e, request)
File "/home/zvector/BlogLearn/venv/local/lib/python2.7/site-packages/django/core/handlers/base.py", line 215, in _get_response
response = response.render()
File "/home/zvector/BlogLearn/venv/local/lib/python2.7/site-packages/django/template/response.py", line 107, in render
self.content = self.rendered_content
File "/home/zvector/BlogLearn/venv/local/lib/python2.7/site-packages/django/template/response.py", line 82, in rendered_content
template = self.resolve_template(self.template_name)
File "/home/zvector/BlogLearn/venv/local/lib/python2.7/site-packages/django/template/response.py", line 64, in resolve_template
return select_template(template, using=self.using)
File "/home/zvector/BlogLearn/venv/local/lib/python2.7/site-packages/django/template/loader.py", line 53, in select_template
raise TemplateDoesNotExist(', '.join(template_name_list), chain=chain)
TemplateDoesNotExist: blogs/post_list.html
把 index.html 改为 post_list.html
感谢博主,目前访问主页成功,访问其他(归档,分类等)也成功了,但我有个疑问,这样修改的原理是什么?我仔细对照过博主github教程上的代码,主页的文件名称并不需要修改(为index.html),这是为何?这样的改动会不会太大,因为相应的其他地方需要用到index.html文件的地方也需要修改。再次感谢博主的解答。
只是为了方便,所以各个视图函数都渲染了 index.html,如果你的模板不同的话,你可以创建不同的模板,不同视图渲染不同模板就可以了。
感谢,猜到了是2.7兼容的问题还没找出来,幡然醒悟
pagination_data 这个函数感觉博主写的逻辑很严谨,但是是不是有一个小小的问题, rigth并不会取到1, 所以如果当前页面为第一页时, 就没有页码1了, 感觉把right = page_range[page_number:page_number + 2],改为right = page_range[page_number - 1:page_number + 2]是不是好一些。
以上说的是page_num == 1时
应该可以取到 1 吧?我博客用的这段代码,使用下来没发现 bug。你这样修改以后测试有问题么?
想了一下逻辑,page_num == 1时right = page_range[page_num: page_num + 2] 无论如何取不到1,因为page_range[0]为1, 而此时page_num=1, 取到的是2, first此时也一直为false取不到1。亲测也是如此。
当然不排除我可能有bug。。
其实我现在搞不清我写的代码了,总之目前来看运行上没什么问题,也就懒得管了。当然我这个代码里可能有些重复,有可以优化的地方。
我可以给您解释下,page_num==1的时候 first本来就是要false的,您可能还没有明白这个参数的意思,frist的意义是“标示是否需要显示第一页的页码号”,您现在当前页面是第一页的情况,只要判断right和last以及right_has_more这些参数即可,你看看index.html模板下的代码就知道了
这个代码逻辑性已经非常号了
if page_number == 1:
# 用户请求的是第一页的数据
# 页码的右边部分为[2,3],左边和右边的连续页码都是加2
right = page_range[1:3]
不过我觉的直接写[1:3]会更加简洁明了
必须从 page range 里截取吧?因为有时候只有两页。
嗯,我明白了,第一页时直接用数字代替就可以了。
没事的就是超过这个范围也不会有异常的。我都测试过了
我知道first的意思, 我的意思是1不会显示, 因为right里面没有。。。。。
page_range[1] = 2
当你当前页码为1的时候,right肯定不能包含1啊,这位大哥,right的意思是你当前页码右边显示的两个页码数组,怎么可能有1啊??你当前页码是1啊
是的 1不会显示,在模板中显示的是 current page,而实际就是 1
我知道了,理解不同
找出问题了,我已经修改了,感谢讨论
楼上有人解答了,切片取到有效值为止
好吧,说的不是一个问题我理解错了
类似的,这个分页方法是通用的,稍加修改可以用于任何需要分页的地方。
请问用户profile页面可以做吗? 用户follow,私信、动态等功能
这个分页方法是通用的,稍加修改可以用于任何需要分页的地方。