Django Pagination 完善分页

17068 86 2017年6月2日

Django Pagination 简单分页 中,我们实现了一个简单的分页导航效果。但效果有点差强人意,我们只能点上一页和下一页的按钮进行翻页。比较完善的分页效果应该像下面这样,但想实现这样一种效果,Django Pagination 内置的 API 已无能为力。本文将通过拓展 Django Pagination 来实现下图这样比较完善的分页效果。

高级分页效果图

分页效果概述

一个比较完善的分页效果应该具有以下特性,就像上图展示的那样,很多网站都采用了类似这种的分页导航方式。

  • 始终显示第一页和最后一页。
  • 当前页码高亮显示。
  • 显示当前页码前后几个连续的页码。
  • 如果两个页码号间还有其它页码,中间显示省略号以提示用户。

拓展 Pagination

在此之前,我们已将首页文章列表的视图函数转为了类视图,并且使用了类视图 ListView 中已经为我们写好的分页代码来达到分页的目的(详情请查看文章开头处给出的链接)。为了实现如下所展示的分页效果,接下来就需要在 ListView 的基础上进一步拓展分页的逻辑代码。

高级分页效果图

先来分析一下导航条的组成部分,可以看到整个分页导航条其实可以分成 七个部分:

  1. 第 1 页页码,这一页需要始终显示。
  2. 第 1 页页码后面的省略号部分。但要注意如果第 1 页的页码号后面紧跟着页码号 2,那么省略号就不应该显示。
  3. 当前页码的左边部分,比如这里的 3-6。
  4. 当前页码,比如这里的 7。
  5. 当前页码的右边部分,比如这里的 8-11。
  6. 最后一页页码前面的省略号部分。但要注意如果最后一页的页码号前面跟着的页码号是连续的,那么省略号就不应该显示。
  7. 最后一页的页码号。

因此我们的思路是,在视图里将以上七步中所需要的数据生成,然后传递给模板并在模板中渲染显示即可。整个视图的代码如下,由于代码比较长,所以代码实现的功能直接在代码块中注释,就不在文章中进一步说明了。推荐使用大屏幕阅读器获取更好的阅读体验。

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 样式即可:

Django 完善分页效果

总结

本章节的代码位于:Step20: complete pagination

如果遇到问题,请通过下面的方式寻求帮助。

-- EOF --

最后更新:2018-11-11 12:21:12

86 条评论 / 39 人参与
sLee0709

博主您好。

非常感谢您的教程。有个问题想咨询您一下,一开始我通过您的教程能够成功实现刷新页面分页,然后我通过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实现异步分页功能呢?

望博主不吝赐教,十分感谢!


sLee0709 sLee0709

抱歉,我忘了,我html中的代码跟您略有不同,如下所示:

{% if is_paginated %}
<div class="ui pagination menu" id="p_manu_num">
{% if first %}
<a class="item" name="1">1</a>
{% endif %}
{% if left %}
{% if left_more %}
<div class="disabled item">...</div>
{% endif %}
{% for i in left %}
<a class="item" name="{{ i }}">{{ i }}</a>
{% endfor %}
{% endif %}
<a class="item active" id="act" name="{{ page_obj.number }}"><b>{{ page_obj.number }}</b></a>
{% if right %}
{% for j in right %}
<a class="item" name="{{ j }}">{{ j }}</a>
{% endfor %}
{% if right_more %}
<div class="disabled item">...</div>
{% endif %}
{% endif %}
{% if last %}
<a class="item" name="{{ paginator.num_pages }}">{{ paginator.num_pages }}</a>
{% endif %}
</div>
{% endif %}

BigOrange128

请问,为什么访问前三页分页没有问题,后面的就无法访问了。

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 &amp; Blog</title>

8 <style>

9 span.highlighted{

10 color: #00CCFF;


BigOrange128 BigOrange128

解决了,下面评论的简洁代码有bug。


热心市民·沈先生 BigOrange128

请问你怎么解决的哈,我遇到了跟你一样的问题。我用的是博主的代码,不是下面评论的代码。


DFnum26
提示下:楼上发的CSS样式需要放在base.html的<head></head>中,且用<style type="text/css"></style>包含其中CSS代码!

SamK6517433923 DFnum26

CSS可以了,感谢~


Rich

博主您好,请问如果我没有使用您的通用类视图,要如何写高级的分页功能呢? 请问能给个思路或案例嘛?非常感谢您~~~ :)


Rich Rich

哈哈 我搞定啦! 谢谢楼主啦~~  看到文章内那么多代码心里害怕了,但是其实仔细读一读是可以读懂的


庄鑫王建

楼主您好!请问分页的居中如何设置呢?我在div中使用 align="center"但是没有效果。谢谢!


jizhongwei

博主,还想没有实现当前页的页码颜色加深哦。。。。


追梦人物 [博主] jizhongwei

参考评论区的代码,自行添加 css 即可。


花_易秋
  •  是大法官
  • Image

测试样例


Me_Before丨You

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)


孤云飘飘zhao

老师的思路非常好,我来顺便简化一下一些重复的代码.

def pagination_data(self, paginator, page, is_paginated):
if not is_paginated:
return {}
left_has_more = False
right_has_more = False
first = False
last = False
page_number = page.number
total_pages = paginator.num_pages
page_range = paginator.page_range
right = page_range[page_number:page_number + 2]
left = page_range[(page_number - 3) if (page_number - 3) > 0 else 0:page_number - 1]
if right:
if right[-1] < total_pages - 1:
right_has_more = True
if right[-1] < total_pages:
last = True
if left:
if left[0] > 2:
left_has_more = True
if left[0] > 1:
first = True

郭效杨

那这样看起来只有index主页有分页啊进入标签页,类别页会调用index view中的分页吗?


双鱼幸福leon 郭效杨

分类和归档用之前那的视图类继承

class ArchivesView(IndexView)