在学习完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})
今后在不同的视图函数内,均可以导入该分页器,会自动将查询结果和对应分页属性传入模板,只需要修改模板内数据展示的部分即可.