拓展 Django Pagination 实现完善的分页效果

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

因此我们的思路是,在视图里将以上七步中所需要的数据生成,然后传递给模板在模板中渲染显示就行。整个视图的代码如下,代码实现的功能已有详细注释,就不在文章中进一步说明了。

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 --


6 条评论 / 5 人参与
Kakuchange
仅供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捕获异常。。下面为实际改动部分,仅供参考~
```python
else:
left = range(page_range[
(page_number - 2) if (page_number - 2) > 0 else 0], page_range[page_number - 1])
try:
right = range(page_range[page_number], page_range[
page_number + 1])
except Exception as e:
right = range(page_range[page_number], page_range[total_pages-1])
try:
if right[-1] < total_pages - 1:
right_has_more = True
if right[-1] < total_pages:
last = True
except Exception as e:
last = True
if left[0] > 2:
left_has_more = True
if left[0] > 1:
first = True
```

Kakuchange Kakuchange
!注:为了测试分布效果,设置paginate_by = 1,具体设置为别的还需再做调整

forgood一下sj
super() takes at least 1 argument (0 given)
这是为什么啊

追梦人物 [博主] forgood一下sj
Python2 请使用 python2 的super 函数写法,具体请百度或者查看 py2文档。

我是单永旭
博主确实厉害,学习了,今天刚看了一下paginator,能实现基本功能,但是像博主这样的功能没什么思路,看了一下博主的想法,很有借鉴性