在学习完Django的MTV模型后,知道了Django的架构,可以写出复杂的应用了.但是离一个完整的web站点,还差了最核心的比如站点管理和用户等功能.因此还需要知道Django中与用户和状态相关的高级功能,以及其他一些高级操作.

分页

分页是很多内容管理站点必须有的功能,像博客和论坛,一般是在每页上展示固定数量的内容,然后通过翻页前后翻动,如果在一页上展示太多内容,用户体验会很差.
我们先通过 bulk_create方式大量创建一些书,然后用一个页面将其一次性展示出来,可以看到用户体验很差.

通过切片进行分页

如果想每页显示10个,首先想到的是在视图函数内按照排序取得所有对象之后,由于QuerySet是一个列表,只要进行切片就可以了.第n页的索引范围是[(n-1)*10:n*10].那么关键就是要取到n,可以通过页面里让用户选第几页,然后返回?page=n这样来取得.
然后还一个问题,就是一共多少页呢,这里可以很方便的对QuerySet使用.count()方法获取总数,然后用总数除以10并且取商和余数,如果余数为0,则商就是页数,如果余数不为0,总页数就是商+1.
根据数量,生成一个按钮的清单表示第几页.注意,如果余数不为0,实际上最后一页的索引就应该单独处理,如果余数为0,则不用特殊处理,每页显示相同的页面.
此外,引入了Bootstrap的分页组件,分页组件除了页数之外,还有一个前一页和后一页,这两个也很简单,将当前的页面传入模板,然后判断一下,当页数为1的时候上一页依然是1,当页数为最后一页的时候下一页依然是最后一页.
最后还有一个active状态,同样只要写一个if判断即可.

基础的分页逻辑

def book_list(request):
    # 用一个变量表示每页显示的数量
    page_per_html = 10
    # 取得GET方法传入的当前页数
    page_num = int(request.GET.get("page", 1))
    # 取得数据库内所有要显示的记录的数量
    all_book = models.Book.objects.all().order_by("id")
    all_book_num = all_book.count()
    # 计算商和余数,用于计算总共有几页和最后一页的处理
    page, last = divmod(all_book_num, page_per_html)
    # 余数为0,则为整数页面,计算出页面中需要的各个元素.
    if last == 0:
        page_li = [i + 1 for i in range(page)]
        previous = page_num - 1 if page_num - 1 >= 1 else 1
        next = page_num + 1 if page_num + 1 <= page else page all_book = all_book[(page_num - 1) * page_per_html:page_num * page_per_html] 
        return render(request, "books.html", {"book_list": all_book, "page": page_li, "previous": previous, "next": next, "current": page_num}) 
 
    else: 
        # 余数为1,则页面数需要加1,同时对最后一页进行特别处理
        page += 1 
        page_li = [i + 1 for i in range(page)] 
        previous = page_num - 1 if page_num - 1 >= 1 else 1
        next = page_num + 1 if page_num + 1 <= page else page

        if page_num != page:
            all_book = all_book[(page_num - 1) * page_per_html:page_num * page_per_html]
            return render(request, "books.html",
                          {"book_list": all_book, "page": page_li, "previous": previous, "next": next,
                           "current": page_num})
        else:
            all_book = all_book[(page_num - 1) * page_per_html:(page_num - 1) * page_per_html + last + 1]
            return render(request, "books.html",
                          {"book_list": all_book, "page": page_li, "previous": previous, "next": next,
                           "current": page_num})

页面编写如下:

<div class="container">
    <h1 class="text-center">书籍列表</h1>

    <table class="table table-striped table-bordered tab">
        <thead>
        <tr>
            <th>序号</th>
            <th>id</th>
            <th>书名</th>
        </tr>
        </thead>

        <tbody>
        {% for book in book_list %}
            <tr>
                <td>{{ forloop.counter }}</td>
                <td>{{ book.id }}</td>
                <td>{{ book.title }}</td>
            </tr>
        {% endfor %}
        </tbody>
    </table>

    <div class="btn btn-group">

    </div>
    <nav aria-label="Page navigation">
        <ul class="pagination">
            <li>
                <a href="/books/?page={{ previous }}" aria-label="Previous">
                    <span aria-hidden="true">«</span>
                </a>
            </li>
            {% for i in page %}
                <li><a href="/books/?page={{ i }}"
                       class="btn btn-primary {% if i == current %}active{% endif %} }">{{ i }}</a></li>

            {% endfor %}

            <li>
                <a href="/books/?page={{ next }}" aria-label="Next">
                    <span aria-hidden="true">»</span>
                </a>
            </li>
        </ul>
    </nav>
</div>

目前记录只有100多条,每页显示10条,看上去还比较合理,如果内容比较多,比如有1000条,会发现分页导航条会显示一大片,而成熟的网页一般会控制总共展示的页码数量.经过分析,只需要修改视图函数,控制传入给页面生成页码导航条的语句的列表即可.

改进每页显示固定的页码数量

def book_list(request):
    page_per_html = 5
    page_num = int(request.GET.get("page", 1))
    all_book = models.Book.objects.all().order_by("id")
    all_book_num = all_book.count()
    max_page = 11
    half_num = max_page//2
    page, last = divmod(all_book_num, page_per_html)
    if last == 0:
    # 新的修改在这里,用于控制每次传入分页导航的列表,长度固定为11,当前页小于5的时候,固定是range(11)+1也就是1到11,当前页大于总页数-5的时候,固定是range(页数-11,页数)+1,即最后11页.其他的情况,就从当前页-6到当前页+5的结果再加1,正好就是以当前页面左右各5的数字.
        if page_num - half_num < 1: page_li = [i + 1 for i in range(max_page)] elif page_num + half_num > page:
            page_li = [i + 1 for i in range(page - max_page, page)]
        else:
            page_li = [i + 1 for i in range(page_num - half_num-1, page_num + half_num)]

        previous = page_num - 1 if page_num - 1 >= 1 else 1
        next = page_num + 1 if page_num + 1 <= page else page
        all_book = all_book[(page_num - 1) * page_per_html:page_num * page_per_html]
        return render(request, "books.html",
                      {"book_list": all_book, "page": page_li, "previous": previous, "next": next, "current": page_num})
    else:
        page += 1
    # 新的修改与上边一样,只是page数不同.
        if page_num - half_num < 1: page_li = [i + 1 for i in range(max_page)] elif page_num + half_num > page:
            page_li = [i + 1 for i in range(page - max_page, page)]
        else:
            page_li = [i + 1 for i in range(page_num - half_num-1, page_num + half_num)]

        previous = page_num - 1 if page_num - 1 >= 1 else 1
        next = page_num + 1 if page_num + 1 <= page else page

        if page_num != page:
            all_book = all_book[(page_num - 1) * page_per_html:page_num * page_per_html]
            return render(request, "books.html",
                          {"book_list": all_book, "page": page_li, "previous": previous, "next": next,
                           "current": page_num})
        else:
            all_book = all_book[(page_num - 1) * page_per_html:(page_num - 1) * page_per_html + last + 1]
            return render(request, "books.html",
                          {"book_list": all_book, "page": page_li, "previous": previous, "next": next,
                           "current": page_num})

这样就初步做好了一个分页的功能,通过page_per_html变量控制每页显示的记录,通过将max_page设置为奇数,half_num = max_page//2的关系,让导航条以当前页面居中显示.还加上了导航条上当前页面对应的链接被激活的状态.现在只需要添加上第一页和最后一页的功能就可以了.需要先在模板上增加两个固定的按钮,然后对其传入两个新的变量.这两个变量的固定值一个是1,一个是总分页数.
修改模板如下:

<div class="container">
    <h1 class="text-center">书籍列表</h1>

    <table class="table table-striped table-bordered tab">
        <thead>
        <tr>
            <th>序号</th>
            <th>id</th>
            <th>书名</th>
        </tr>
        </thead>

        <tbody>
        {% for book in book_list %}
            <tr>
                <td>{{ forloop.counter }}</td>
                <td>{{ book.id }}</td>
                <td>{{ book.title }}</td>
            </tr>
        {% endfor %}
        </tbody>
    </table>

    <div class="btn btn-group">

    </div>
    <nav aria-label="Page navigation">
        <ul class="pagination">
            <li>
                <a href="/books/?page={{ previous }}" aria-label="Previous">
                    <span aria-hidden="true">«</span>
                </a>
            </li>
            <li>
                <a href="/books/?page={{ start }}" aria-label="Previous">
                    <span aria-hidden="true">首页</span>
                </a>
            </li>
            {% for i in page %}
                <li><a href="/books/?page={{ i }}"
                       class="btn btn-primary {% if i == current %}active{% endif %} }">{{ i }}</a></li>

            {% endfor %}
            <li>
                <a href="/books/?page={{ end }}" aria-label="Next">
                    <span aria-hidden="true">尾页</span>
                </a>
            </li>

            <li>
                <a href="/books/?page={{ next }}" aria-label="Next">
                    <span aria-hidden="true">»</span>
                </a>
            </li>
        </ul>
    </nav>
</div>

之前的函数逻辑有些混乱,需要重新整理一下,合并一些分支判断.
重新来想一下,在函数的初始部分,需要增加对用户传入参数错误或者越界的处理.然后需要拿到若干个变量供后边使用,核心的变量就是每页需要展示的数量,分成几页,最后一页展示多少数量,页码最多展示几个.
基于上边这些变量,通过每次页面传入的当前值,生成一个序列传递给模板的导航条部分即可.
重新整理的视图函数如下:

def book_list(request):
    # 每页显示的数据数量
    page_per_html = 7
    all_book = models.Book.objects.all().order_by("id")
    all_book_num = all_book.count()
    # 每页显示的最多页码数量
    max_page = 11
    # 中心页码左右两边数量
    half_num = max_page // 2
    # 用divmod 方法计算总页码,以及最后一页显示的数据数量
    page, last = divmod(all_book_num, page_per_html)
    if last:
        page += 1
    # 如果总页码小于每页显示的页面数量,则就显示总页码的数量
    if max_page >= page:
        max_page = page
    # 错误处理,如果用户传入非数字,0和负数,跳到第一页,如果传入大于最后页码的数字,跳到最后一页
    try:
        page_num = int(request.GET.get("page", 1))
        if page_num > page:
            page_num = page
        if page_num <= 0:
            page_num = 1
    except Exception:
        page_num = 1
    # 用于判断具体页码的显示,小于或者大于一定边界后就不再随位置显示
    if page_num - half_num < 1: page_li = [i + 1 for i in range(max_page)] elif page_num + half_num > page:
        page_li = [i + 1 for i in range(page - max_page, page)]
    else:
        page_li = [
            i +
            1 for i in range(
                page_num -
                half_num -
                1,
                page_num +
                half_num)]
    # 确定上一页和下一页,第一页的上一页依然是第一页,最后一页的下一页依然是最后一页
    previous = page_num - 1 if page_num - 1 >= 1 else 1
    next = page_num + 1 if page_num + 1 <= page else page

    # 如果不是最后一页,则展示每页数量,或者没有余数,正好每页可以放下,都展示正常数据
    if page_num != page or last == 0:
        print(page_num, page)
        all_book = all_book[(page_num - 1) *
                            page_per_html:page_num * page_per_html]
        return render(request,
                      "books.html",
                      {"book_list": all_book,
                       "page": page_li,
                       "previous": previous,
                       "next": next,
                       "current": page_num,
                       "start": 1,
                       "end": page})
    # 当是最后一页而且有余数,对于该页只展示余数的部分
    else:
        all_book = all_book[(
                                    page_num - 1) * page_per_html:(page_num - 1) * page_per_html + last + 1]
        return render(request,
                      "books.html",
                      {"book_list": all_book,
                       "page": page_li,
                       "previous": previous,
                       "next": next,
                       "current": page_num,
                       "start": 1,
                       "end": page})

封装成可复用的代码块

编写完成以后,如果每次要分页,必须要把这段代码粘贴到视图函数里边,重复工作太高.
观察视图函数,可以发现,视图函数在启动的时候需要获得的变量是:每页显示数量,每页显示的页码数,要查询的数据总数,当前页码.剩下的内部变量都是通过这几个变量计算出来的.再观察视图函数的输出,对模板传递的值是:查询数据的切片结果集,导航条的数字列表,下一个页码,上一个页码,当前页码,首页页码,最后一页页码.

那么可以封装一个类,在其他的视图函数中调用.只要这个视图函数把每页显示的数量,每页显示的页面量,要查询那张表,以及通过GET方法获得当前页码当做变量传递进来,就可以通过类的属性提供上边的这些输出,然后模板内可以把导航条的部分固定下来.只需要根据得到的数据的具体字段进行展示就可以了.由于到哪个表去检索数据也需要传入,而传入对象不太好处理,这里改成数据的开始和结束索引,让视图函数自己去控制检索什么内容.

列一个表如下:

传入参数 传出结果
每页显示数量 切片开始索引
每页显示页码数 切片结束索引
数据总数 页码数字列表
当前页码 下一个页码
上一个页码
当前页码
首页页码
末页页码

再观察视图函数的结构,根据if page_num != page or last == 0:判断的结果只影响传给模板的结果集,其他的部分都是计算好的.按照该思路来组织类:

class PageDivider:
    def __init__(
            self,
            current_page,
            data_length,
            data_per_page=10,
            max_page=11):
    # 在初始化函数中计算好后边要使用的变量,只有返回的开始和结束索引需要在函数中计算,逻辑与视图函数中一样
        self.page_li = []
        self.data_num = data_length
        self.per_page = data_per_page
        self.nav_length = max_page
        self.half_num = self.nav_length // 2
        self.page, self.last = divmod(self.data_num, self.per_page)
        if self.last:
            self.page += 1
        try:
            self.current = int(current_page)
            if self.current > self.page:
                self.current = self.page
            if self.current <= 0: self.current = 1 except Exception: self.current = 1 if self.nav_length >= self.page:
            self.nav_length = self.page

        self.pre = self.current - 1 if self.current - 1 >= 1 else 1
        self.nex = self.current + 1 if self.current + 1 <= self.page else self.page

    # 返回最后一个页面的页码
    @property
    def last_page_num(self):
        return self.page
    # 返回第一个页面的页码,固定为1
    @property
    def first_page_num(self):
        return 1
    # 返回当前页面的页码
    @property
    def current_page(self):
        return self.current
    # 返回导航条所需要的数字列表
    @property
    def nav_list(self):
        if self.current - self.half_num < 1:
            self.page_li = [i + 1 for i in range(self.nav_length)] 
        elif self.current + self.half_num > self.page:
            self.page_li = [i + 1 for i in range(self.page - self.nav_length, self.page)]
        else:
            self.page_li = [
                i +
                1 for i in range(
                    self.current -
                    self.half_num -
                    1,
                    self.current +
                    self.half_num)]
        return self.page_li
    # 返回上一个按钮对应的页码
    @property
    def previous(self):
        return self.pre
    # 返回下一个按钮对应的页码
    @property
    def next_num(self):
        return self.nex
    # 返回页面使用的数据切片的开始索引
    @property
    def start_index(self):
        return (self.current - 1) * self.per_page
    # 返回页面使用的数据切片的结束索引,根据情况来返回最后一页的内容
    @property
    def end_index(self):
        if self.current != self.page or self.last == 0:
            return self.current * self.per_page
        else:
            return (self.current - 1) * self.per_page + self.last + 1

这个类写在app下边自定义名称的py文件里,在使用的时候只需要导入即可.
完成了这个类之后,在视图函数中,只需要通过当前页面和需要查找的数据总量实例化一个分页器对象,后边的每页展示数据默认为10,页码默认显示11个页码,之后将分页器的各个属性传递给模板即可.
修改后的模板如下:

<div class="container">
{#标题部分#}
    <h1 class="text-center">书籍列表</h1>
{#数据展示部分#}
    <table class="table table-striped table-bordered tab">
        <thead>
        <tr>
            <th>序号</th>
            <th>id</th>
            <th>书名</th>
        </tr>
        </thead>

        <tbody>
        {% for book in book_list %}
            <tr>
                <td>{{ forloop.counter }}</td>
                <td>{{ book.id }}</td>
                <td>{{ book.title }}</td>
            </tr>
        {% endfor %}
        </tbody>
    </table>
{#分页条部分#}
    <nav aria-label="Page navigation">
        <ul class="pagination">
            <li class="{% if current == 1 %}disabled{% endif %}">
                <a href="/books/?page={{ previous }}"  aria-label="Previous">
                    <span aria-hidden="true">«</span>
                </a>
            </li>
            <li>
                <a href="/books/?page={{ start }}" aria-label="Previous">
                    <span aria-hidden="true">首页</span>
                </a>
            </li>
            {% for i in page %}
                <li><a href="/books/?page={{ i }}"
                       class="btn btn-primary {% if i == current %}active{% endif %} }">{{ i }}</a></li>

            {% endfor %}
            <li>
                <a href="/books/?page={{ end }}">
                    <span aria-hidden="true">尾页</span>
                </a>
            </li>

            <li class="{% if current == end %}disabled{% endif %}">
                <a href="/books/?page={{ next }}">
                    <span aria-hidden="true">»</span>
                </a>
            </li>
        </ul>
    </nav>
</div>

模板的导航条内需要的参数对应分页器类的各个属性,传入的数据的切片也是配合导航条生成的,今后只需要修改数据展示部分.
修改后的视图函数如下:

# 从自己编写的文件中导入分页器类
from book_manage.pagedivide import PageDivider

def book_list2(request):
    from book_manage.pagedivide import PageDivider
    # data_per_page = 15
    # max_page = 9
    current_page = request.GET.get("page", 1)
    all_book = models.Book.objects.all()
    all_book_num = all_book.count()
    page_divider = PageDivider(current_page, all_book_num)

    return render(request,
                  "books.html",
                  {"book_list": all_book[page_divider.start_index:page_divider.end_index],
                   "page": page_divider.nav_list,
                   "previous": page_divider.previous,
                   "next": page_divider.next_num,
                   "current": page_divider.current_page,
                   "start": page_divider.first_page_num,
                   "end": page_divider.last_page_num})

今后在不同的视图函数内,均可以导入该分页器,会自动将查询结果和对应分页属性传入模板,只需要修改模板内数据展示的部分即可.