前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >拓展 Django Pagination 实现完善的分页效果

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

作者头像
追梦人物
发布2018-04-17 14:42:29
1.9K0
发布2018-04-17 14:42:29
举报
文章被收录于专栏:Django中文社区Django中文社区

使用 Django Pagination 实现简单的分页功能 中,我们实现了一个简单的分页导航效果。但想实现下面这样的一个比较完善的分页导航时,Django Pagination 内置的 API 已经无法满足需求。本文将通过拓展 Django Pagination 来实现下图这样比较完善的分页效果。

分页效果概述

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

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

类视图 ListView

由于在开发网站的过程中,有一些视图函数虽然处理的对象不同,但是其大致的代码逻辑是一样的。比如一个博客和一个论坛,通常其首页都是展示一系列的文章列表或者帖子列表。对处理首页的视图函数来说,虽然其处理的对象一个是文章,另一个是帖子,但是其处理的过程是非常类似的。首先是从数据库取出文章或者帖子列表,然后将这些数据传递给模板并渲染模板。

于是 Django 把这些相同的逻辑代码抽取了出来,写成了一系列的通用视图函数,即基于类的通用视图。本文将使用到通用视图 ListView。ListView 用来从数据库获取一个对象列表,而对列表进行分页的过程也是比较通用的,ListView 已经实现了分页功能。所以我们直接使用 ListView 而不是自己写分页逻辑,以达到代码复用的目的。

ListView 的使用非常简单,只需要将你自己的视图继承 ListView ,然后复写一些属性和方法即可。例如我们博客的首页视图 index 代码如下:

代码语言:javascript
复制
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})

现在将其转化为等价的类视图如下:

代码语言:javascript
复制
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 的配置一开始是这样的:

代码语言:javascript
复制
blog/urls.py

app_name = 'blog'
urlpatterns = [
    url(r'^$', views.index, name='index'),
    ...
]

url 函数接收的一个参数是一个正则表达式,用于匹配用户请求的 URL 模式。第二个参数是被调用的视图函数,其类型必须是一个函数。而我们写的 IndexView 视图是一个类,为了将其转换成一个函数,只需要调用其父类中的 as_view 方法即可。因此将 URL 的配置改为:

代码语言:javascript
复制
blog/urls.py

app_name = 'blog'
urlpatterns = [
    url(r'^$', views.IndexView.as_view(), name='index'),
    ...
]

ListView 分页后会给模板传递一个 is_paginated 和一个 page 变量。前者用于标示是否分页,后者是一个 Page 对象。因此在模板中设置一个简单的分页导航如下:

代码语言:javascript
复制
{% 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. 最后一页的页码号。

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

代码语言:javascript
复制
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

模板中设置分页导航

接下来便是在模板中设置分页导航了,将导航条的七个部分一一展现即可,示例代码如下:

代码语言:javascript
复制
{% 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 样式即可。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 分页效果概述
  • 类视图 ListView
  • 拓展 Pagination
  • 模板中设置分页导航
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档