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