我们可以非常容易地使用 CBV 来实现分页功能。但首先我想手工分页,这样就更有助于我们理解背后的机制,这样它就不那么神秘了。
实际上对 boards 列表视图分页并没有意义,因为我们不期望有很多 boards。但无疑对于主题列表和帖子列表来说是需要一些分页的。
从现在起,我们将在 board_topics 这个视图中来操作。
首先,我们添加一些帖子。我们可以直接使用应用程序的用户界面来添加几个帖子,或者打开 python shell 编写一个小脚本来为我们完成:
python manage.py shell
from django.contrib.auth.models import User
from boards.models import Board, Topic, Post
user = User.objects.first()
board = Board.objects.get(name='Django')
for i in range(100):
subject = 'Topic test #{}'.format(i)
topic = Topic.objects.create(subject=subject, board=board, starter=user)
Post.objects.create(message='Lorem ipsum...', topic=topic, created_by=user)
很好,现在我们有一些数据可以玩了。
在我们返回去写代码之前,让我们用 python shell 来做一些更多的实验:
python manage.py shell
from boards.models import Topic
# 应用程序中的所有主题
Topic.objects.count()
103
# Django 板块的主题
Topic.objects.filter(board__name='Django').count()
103
# 让我们将所有主题保存到一个变量queryset中。
queryset = Topic.objects.filter(board__name='Django').order_by('-last_updated')
定义一个你要分页的查询集(QuerySet)的排序是很重要的。否则,会返回给你错误的结果。
现在让我们导入 Paginator 工具:
from django.core.paginator import Paginator
paginator = Paginator(queryset, 20)
这里我们告诉Django将查询集按照每页20个元素分页。现在让我们来研究一些 paginator 的属性:
# 计算paginator中的元素个数
paginator.count
103
# 总共103 个元素, 每个页面20个元素一共有6页
# 最后一页仅有3个元素
paginator.num_pages
6
# 可以用来迭代和创建模板中页面链接的页面范围。
paginator.page_range
range(1, 7)
# 返回一个页面实例
paginator.page(2)
<Page 2 of 6>
page = paginator.page(2)
type(page)
django.core.paginator.Page
type(paginator)
django.core.paginator.Paginator
这里我们必须注意,因为如果我们试图找到一个不存在的页面,分页器会抛出一个异常:
paginator.page(7)
EmptyPage: That page contains no results
或者如果我们随意传递进去一个不是页码数字的参数,也会报错:
paginator.page('abc')
PageNotAnInteger: That page number is not an integer
我们必须在设计用户界面时牢记这些细节。
我们来简单看一下 Page 类提供的属性和方法:
page = paginator.page(1)
# 检查这一页后是否还有另一页。
page.has_next()
True
# 如果没有前一页,这意味着这是第一页。
page.has_previous()
False
page.has_other_pages()
True
page.next_page_number()
2
# 注意这里,因为没有上一页,
# 如果我们调用方法的previous_page_number(),我们将得到一个异常:
page.previous_page_number()
EmptyPage: That page number is less than 1
FBV 分页
这里是我们如何使用 FBV 来实现分页:
boards/views.py
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
def board_topics(request, pk):
board = get_object_or_404(Board, pk=pk)
queryset = board.topics.order_by('-last_updated').annotate(replies=Count('posts') - 1)
page = request.GET.get('page', 1)
paginator = Paginator(queryset, 20)
try:
topics = paginator.page(page)
except PageNotAnInteger:
# fallback to the first page
topics = paginator.page(1)
except EmptyPage:
# probably the user tried to add a page number
# in the url, so we fallback to the last page
topics = paginator.page(paginator.num_pages)
return render(request, 'topics.html', {'board': board, 'topics': topics})
这部分的实现是使用了 Bootstrap 的四个分页组件来正确的渲染页面。你需要花时间阅读代码,看看它是否适合你。我们在这里使用的是我们之前用过的方法。在这种情况下,topics
不再是一个查询集(QuerySet
),而是一个 paginator.page
的实例。
在 topics HTML列表的基础上,我们可以渲染分页组件:
templates/topics.html
{% if topics.has_other_pages %}
<nav aria-label="Topics pagination" class="mb-4">
<ul class="pagination">
{% if topics.has_previous %}
<li class="page-item">
<a class="page-link" href="?page={{ topics.previous_page_number }}">前一个</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">前一个</span>
</li>
{% endif %}
{% for page_num in topics.paginator.page_range %}
{% if topics.number == page_num %}
<li class="page-item active">
<span class="page-link">
{{ page_num }}
<span class="sr-only">(current)</span>
</span>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="?page={{ page_num }}">{{ page_num }}</a>
</li>
{% endif %}
{% endfor %}
{% if topics.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ topics.next_page_number }}">下一个</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">下一个</span>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
GCBV 分页
下面,相同的实现,但这次使用ListView。
boards/views.py
class TopicListView(ListView):
model = Topic
context_object_name = 'topics'
template_name = 'topics.html'
paginate_by = 20
def get_context_data(self, **kwargs):
kwargs['board'] = self.board
return super().get_context_data(**kwargs)
def get_queryset(self):
self.board = get_object_or_404(Board, pk=self.kwargs.get('pk'))
queryset = self.board.topics.order_by('-last_updated').annotate(replies=Count('posts') - 1)
return queryset
在使用基于类的视图分页时,我们与模板中paginator进行交互的方式有点不同。它会在模板中提供以下变量:paginator,page_obj,is_paginated,object_list,还有一个我们在 context_object_name 中定义名字的变量。在我们的例子中,这个额外的变量将被命名为 topics ,并且它将等同于 object_list。
关于这个 get_context_data ,其实,它就是我们在扩展 GCBV 时向请求上下文添加内容的方式。
但这里的主要是 paginate_by 属性。一般情况下,只需添加它就足够了。
要记得更新 urls.py 哦:
myproject/urls.py
from django.conf.urls import url
from boards import views
urlpatterns = [
# ...
url(r'^boards/(?P<pk>\d+)/$', views.TopicListView.as_view(), name='board_topics'),
]
现在我们来修改一下模板:
templates/topics.html
{% block content %}
<div class="mb-4">
<a href="{% url 'new_topic' board.pk %}" class="btn btn-primary">新主题</a>
</div>
<table class="table mb-4">
<!-- table content suppressed -->
</table>
{% if is_paginated %}
<nav aria-label="Topics pagination" class="mb-4">
<ul class="pagination">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">前一个</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">前一个</span>
</li>
{% endif %}
{% for page_num in paginator.page_range %}
{% if page_obj.number == page_num %}
<li class="page-item active">
<span class="page-link">
{{ page_num }}
<span class="sr-only">(current)</span>
</span>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="?page={{ page_num }}">{{ page_num }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}">下一个</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">下一个</span>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% endblock %}
现在花点时间运行一下测试代码,如果有需要调整的地方就修一下。
boards/tests/test_view_board_topics.py
from django.test import TestCase
from django.urls import resolve
from ..views import TopicListView
class BoardTopicsTests(TestCase):
# ...
def test_board_topics_url_resolves_board_topics_view(self):
view = resolve('/boards/1/')
self.assertEquals(view.func.view_class, TopicListView)
可复用的分页模板
就像我们在 form.html 中封装模板时做的一样,我们也可以为分页的HTML代码片创建类似的东西。
我们来对主题帖子页面进行分页,进而找到一种复用分页组件的方法。
boards/views.py
class PostListView(ListView):
model = Post
context_object_name = 'posts'
template_name = 'topic_posts.html'
paginate_by = 2
def get_context_data(self, **kwargs):
self.topic.views += 1
self.topic.save()
kwargs['topic'] = self.topic
return super().get_context_data(**kwargs)
def get_queryset(self):
self.topic = get_object_or_404(Topic, board__pk=self.kwargs.get('pk'), pk=self.kwargs.get('topic_pk'))
queryset = self.topic.posts.order_by('created_at')
return queryset
更新一下 url.py [查看完整文件]
from django.conf.urls import url
from boards import views
urlpatterns = [
# ...
url(r'^boards/(?P<pk>\d+)/topics/(?P<topic_pk>\d+)/$', views.PostListView.as_view(), name='topic_posts'),
]
现在,我们从topics.html模板中获取分页部分的html代码片,并在 templates/includes 文件夹下面创建一个名为 pagination.html 的新文件,和 forms.html 同级目录:
myproject/
|-- myproject/
| |-- accounts/
| |-- boards/
| |-- myproject/
| |-- static/
| |-- templates/
| | |-- includes/
| | | |-- form.html
| | | +-- pagination.html <-- here!
| | +-- ...
| |-- db.sqlite3
| +-- manage.py
templates/includes/pagination.html
{% if is_paginated %}
<nav aria-label="Topics pagination" class="mb-4">
<ul class="pagination">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">前一个</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">前一个</span>
</li>
{% endif %}
{% for page_num in paginator.page_range %}
{% if page_obj.number == page_num %}
<li class="page-item active">
<span class="page-link">
{{ page_num }}
<span class="sr-only">(current)</span>
</span>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="?page={{ page_num }}">{{ page_num }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}">下一个</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">下一个</span>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
现在,我们在 topic_posts.html 文件中来使用它:
templates/topic_posts.html
{% block content %}
<div class="mb-4">
<a href="{% url 'reply_topic' topic.board.pk topic.pk %}" class="btn btn-primary" role="button">Reply</a>
</div>
{% for post in posts %}
<div class="card {% if forloop.last %}mb-4{% else %}mb-2{% endif %} {% if forloop.first %}border-dark{% endif %}">
{% if forloop.first %}
<div class="card-header text-white bg-dark py-2 px-3">{{ topic.subject }}</div>
{% endif %}
<div class="card-body p-3">
<div class="row">
<div class="col-2">
<img src="{% static 'img/avatar.svg' %}" alt="{{ post.created_by.username }}" class="w-100">
<small>Posts: {{ post.created_by.posts.count }}</small>
</div>
<div class="col-10">
<div class="row mb-3">
<div class="col-6">
<strong class="text-muted">{{ post.created_by.username }}</strong>
</div>
<div class="col-6 text-right">
<small class="text-muted">{{ post.created_at }}</small>
</div>
</div>
{{ post.message }}
{% if post.created_by == user %}
<div class="mt-3">
<a href="{% url 'edit_post' post.topic.board.pk post.topic.pk post.pk %}"
class="btn btn-primary btn-sm"
role="button">Edit</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
{% include 'includes/pagination.html' %}
{% endblock %}
别忘了修改主循环为 {% for post in posts %}
。
我们同样也可以更新一下先前的模板,topics.html 模板同样也可以这个封装的分页模板。
templates/topics.html 查看完整文件
{% block content %}
<div class="mb-4">
<a href="{% url 'new_topic' board.pk %}" class="btn btn-primary">New topic</a>
</div>
<table class="table mb-4">
<!-- table code suppressed -->
</table>
{% include 'includes/pagination.html' %}
{% endblock %}
为了测试目的,你需要添加一些帖子(或者通过 python shell 去创建),然后修改代码中的 ** paginate_by** 到一个较小的数字,比如 2 ,然后看看页面会发生什么变化。
更新一下测试用例:
boards/tests/test_view_topic_posts.py
from django.test import TestCase
from django.urls import resolve
from ..views import PostListView
class TopicPostsTests(TestCase):
# ...
def test_view_function(self):
view = resolve('/boards/1/topics/1/')
self.assertEquals(view.func.view_class, PostListView)
原文:https://github.com/pythonzhichan/django-beginners-guide/blob/master/ClassBasedViews1.md
网友评论