Django入门与实践-第23章:分页实现

    实际上对 boards 列表视图分页并没有意义,因为我们不期望有很多 boards。但无疑对于主题列表和帖子列表来说是需要一些分页的。

    从现在起,我们将在 board_topics 这个视图中来操作。

    首先,我们添加一些帖子。我们可以直接使用应用程序的用户界面来添加几个帖子,或者打开 python shell 编写一个小脚本来为我们完成:

    1. from boards.models import Board, Topic, Post
    2. user = User.objects.first()
    3. board = Board.objects.get(name='Django')
    4. for i in range(100):
    5. subject = 'Topic test #{}'.format(i)
    6. topic = Topic.objects.create(subject=subject, board=board, starter=user)
    7. Post.objects.create(message='Lorem ipsum...', topic=topic, created_by=user)

    很好,现在我们有一些数据可以玩了。

    在我们返回去写代码之前,让我们用 python shell 来做一些更多的实验:

    1. python manage.py shell
    1. from boards.models import Topic
    2. # All the topics in the app
    3. Topic.objects.count()
    4. 107
    5. # Just the topics in the Django board
    6. Topic.objects.filter(board__name='Django').count()
    7. 104
    8. # Let's save this queryset into a variable to paginate it
    9. queryset = Topic.objects.filter(board__name='Django').order_by('-last_updated')

    定义一个你要分页的查询集(QuerySet)的排序是很重要的。否则,会返回给你错误的结果。

    现在让我们导入 Paginator 工具:

    1. from django.core.paginator import Paginator
    2. paginator = Paginator(queryset, 20)

    这里我们告诉Django将查询集按照每页20个元素分页。现在让我们来研究一些 paginator 的属性:

    1. # count the number of elements in the paginator
    2. paginator.count
    3. 104
    4. # total number of pages
    5. # 104 elements, paginating 20 per page gives you 6 pages
    6. # where the last page will have only 4 elements
    7. paginator.num_pages
    8. 6
    9. # range of pages that can be used to iterate and create the
    10. # links to the pages in the template
    11. paginator.page_range
    12. range(1, 7)
    13. # returns a Page instance
    14. paginator.page(2)
    15. <Page 2 of 6>
    16. page = paginator.page(2)
    17. type(page)
    18. django.core.paginator.Page
    19. type(paginator)
    20. django.core.paginator.Paginator

    这里我们必须注意,因为如果我们试图找到一个不存在的页面,分页器会抛出一个异常:

    1. paginator.page(7)
    2. EmptyPage: That page contains no results

    或者如果我们随意传递进去一个不是页码数字的参数,也会报错:

    我们必须在设计用户界面时牢记这些细节。

    我们来简单看一下 Page 类提供的属性和方法:

    1. page = paginator.page(1)
    2. # Check if there is another page after this one
    3. page.has_next()
    4. True
    5. # If there is no previous page, that means this one is the first page
    6. page.has_previous()
    7. False
    8. page.has_other_pages()
    9. True
    10. page.next_page_number()
    11. 2
    12. # Take care here, since there is no previous page,
    13. # if we call the method `previous_page_number() we will get an exception:
    14. page.previous_page_number()
    15. EmptyPage: That page number is less than 1

    这里是我们如何使用 FBV 来实现分页:

    1. from django.db.models import Count
    2. from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
    3. from django.shortcuts import get_object_or_404, render
    4. from django.views.generic import ListView
    5. from .models import Board
    6. def board_topics(request, pk):
    7. board = get_object_or_404(Board, pk=pk)
    8. queryset = board.topics.order_by('-last_updated').annotate(replies=Count('posts') - 1)
    9. page = request.GET.get('page', 1)
    10. paginator = Paginator(queryset, 20)
    11. try:
    12. topics = paginator.page(page)
    13. except PageNotAnInteger:
    14. # fallback to the first page
    15. topics = paginator.page(1)
    16. except EmptyPage:
    17. # probably the user tried to add a page number
    18. # in the url, so we fallback to the last page
    19. topics = paginator.page(paginator.num_pages)
    20. return render(request, 'topics.html', {'board': board, 'topics': topics})

    这部分的实现是使用了 Bootstrap 的四个分页组件来正确的渲染页面。你需要花时间阅读代码,看看它是否适合你。我们在这里使用的是我们之前用过的方法。在这种情况下,topics 不再是一个查询集(QuerySet),而是一个 paginator.page 的实例。

    在 topics HTML列表的基础上,我们可以渲染分页组件:

    templates/topics.html

    1. {% if topics.has_other_pages %}
    2. <nav aria-label="Topics pagination" class="mb-4">
    3. <ul class="pagination">
    4. {% if topics.has_previous %}
    5. <li class="page-item">
    6. <a class="page-link" href="?page={{ topics.previous_page_number }}">Previous</a>
    7. </li>
    8. {% else %}
    9. <li class="page-item disabled">
    10. <span class="page-link">Previous</span>
    11. </li>
    12. {% endif %}
    13. <li class="page-item active">
    14. <span class="page-link">
    15. {{ page_num }}
    16. <span class="sr-only">(current)</span>
    17. </span>
    18. </li>
    19. {% else %}
    20. <li class="page-item">
    21. <a class="page-link" href="?page={{ page_num }}">{{ page_num }}</a>
    22. </li>
    23. {% endif %}
    24. {% endfor %}
    25. {% if topics.has_next %}
    26. <li class="page-item">
    27. <a class="page-link" href="?page={{ topics.next_page_number }}">Next</a>
    28. </li>
    29. {% else %}
    30. <li class="page-item disabled">
    31. <span class="page-link">Next</span>
    32. </li>
    33. {% endif %}
    34. </ul>
    35. </nav>
    36. {% endif %}

    Django入门与实践-第23章:分页实现 - 图1

    下面,相同的实现,但这次使用ListView

    boards/views.py 查看完整文件

    1. class TopicListView(ListView):
    2. model = Topic
    3. context_object_name = 'topics'
    4. template_name = 'topics.html'
    5. paginate_by = 20
    6. def get_context_data(self, **kwargs):
    7. kwargs['board'] = self.board
    8. return super().get_context_data(**kwargs)
    9. def get_queryset(self):
    10. self.board = get_object_or_404(Board, pk=self.kwargs.get('pk'))
    11. queryset = self.board.topics.order_by('-last_updated').annotate(replies=Count('posts') - 1)
    12. 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

    1. from django.conf.urls import url
    2. from boards import views
    3. urlpatterns = [
    4. # ...
    5. url(r'^boards/(?P<pk>\d+)/$', views.TopicListView.as_view(), name='board_topics'),
    6. ]

    现在我们来修改一下模板:

    templates/topics.html 查看完整文件

    1. {% block content %}
    2. <div class="mb-4">
    3. <a href="{% url 'new_topic' board.pk %}" class="btn btn-primary">New topic</a>
    4. </div>
    5. <table class="table mb-4">
    6. <!-- table content suppressed -->
    7. </table>
    8. {% if is_paginated %}
    9. <nav aria-label="Topics pagination" class="mb-4">
    10. <ul class="pagination">
    11. {% if page_obj.has_previous %}
    12. <li class="page-item">
    13. <a class="page-link" href="?page={{ page_obj.previous_page_number }}">Previous</a>
    14. </li>
    15. {% else %}
    16. <li class="page-item disabled">
    17. <span class="page-link">Previous</span>
    18. </li>
    19. {% endif %}
    20. {% for page_num in paginator.page_range %}
    21. {% if page_obj.number == page_num %}
    22. <li class="page-item active">
    23. <span class="page-link">
    24. {{ page_num }}
    25. <span class="sr-only">(current)</span>
    26. </span>
    27. </li>
    28. {% else %}
    29. <li class="page-item">
    30. <a class="page-link" href="?page={{ page_num }}">{{ page_num }}</a>
    31. </li>
    32. {% endif %}
    33. {% endfor %}
    34. {% if page_obj.has_next %}
    35. <li class="page-item">
    36. <a class="page-link" href="?page={{ page_obj.next_page_number }}">Next</a>
    37. </li>
    38. {% else %}
    39. <li class="page-item disabled">
    40. <span class="page-link">Next</span>
    41. </li>
    42. {% endif %}
    43. </ul>
    44. </nav>
    45. {% endif %}
    46. {% endblock %}

    现在花点时间运行一下测试代码,如果有需要调整的地方就修一下。

    就像我们在 form.html 中封装模板时做的一样,我们也可以为分页的HTML代码片创建类似的东西。

    我们来对主题帖子页面进行分页,进而找到一种复用分页组件的方法。

    boards/views.py

    1. class PostListView(ListView):
    2. model = Post
    3. context_object_name = 'posts'
    4. template_name = 'topic_posts.html'
    5. paginate_by = 2
    6. def get_context_data(self, **kwargs):
    7. self.topic.views += 1
    8. self.topic.save()
    9. kwargs['topic'] = self.topic
    10. return super().get_context_data(**kwargs)
    11. def get_queryset(self):
    12. self.topic = get_object_or_404(Topic, board__pk=self.kwargs.get('pk'), pk=self.kwargs.get('topic_pk'))
    13. queryset = self.topic.posts.order_by('created_at')
    14. return queryset

    更新一下 url.py [查看完整文件]

    1. from django.conf.urls import url
    2. urlpatterns = [
    3. # ...
    4. url(r'^boards/(?P<pk>\d+)/topics/(?P<topic_pk>\d+)/$', views.PostListView.as_view(), name='topic_posts'),
    5. ]

    现在,我们从topics.html模板中获取分页部分的html代码片,并在 templates/includes 文件夹下面创建一个名为 pagination.html 的新文件,和 forms.html 同级目录:

    1. myproject/
    2. |-- myproject/
    3. | |-- accounts/
    4. | |-- boards/
    5. | |-- myproject/
    6. | |-- static/
    7. | |-- templates/
    8. | | |-- includes/
    9. | | | |-- form.html
    10. | | | +-- pagination.html <-- here!
    11. | | +-- ...
    12. | |-- db.sqlite3
    13. | +-- manage.py
    14. +-- venv/

    templates/includes/pagination.html

    1. {% if is_paginated %}
    2. <nav aria-label="Topics pagination" class="mb-4">
    3. <ul class="pagination">
    4. {% if page_obj.has_previous %}
    5. <li class="page-item">
    6. <a class="page-link" href="?page={{ page_obj.previous_page_number }}">Previous</a>
    7. </li>
    8. {% else %}
    9. <li class="page-item disabled">
    10. <span class="page-link">Previous</span>
    11. </li>
    12. {% endif %}
    13. {% for page_num in paginator.page_range %}
    14. {% if page_obj.number == page_num %}
    15. <li class="page-item active">
    16. <span class="page-link">
    17. {{ page_num }}
    18. <span class="sr-only">(current)</span>
    19. </span>
    20. </li>
    21. {% else %}
    22. <li class="page-item">
    23. <a class="page-link" href="?page={{ page_num }}">{{ page_num }}</a>
    24. </li>
    25. {% endif %}
    26. {% endfor %}
    27. {% if page_obj.has_next %}
    28. <li class="page-item">
    29. <a class="page-link" href="?page={{ page_obj.next_page_number }}">Next</a>
    30. </li>
    31. {% else %}
    32. <li class="page-item disabled">
    33. <span class="page-link">Next</span>
    34. </li>
    35. {% endif %}
    36. </ul>
    37. </nav>
    38. {% endif %}

    现在,我们在 topic_posts.html 文件中来使用它:

    templates/topic_posts.html

    1. {% block content %}
    2. <div class="mb-4">
    3. <a href="{% url 'reply_topic' topic.board.pk topic.pk %}" class="btn btn-primary" role="button">Reply</a>
    4. </div>
    5. {% for post in posts %}
    6. <div class="card {% if forloop.last %}mb-4{% else %}mb-2{% endif %} {% if forloop.first %}border-dark{% endif %}">
    7. {% if forloop.first %}
    8. <div class="card-header text-white bg-dark py-2 px-3">{{ topic.subject }}</div>
    9. {% endif %}
    10. <div class="card-body p-3">
    11. <div class="row">
    12. <div class="col-2">
    13. <img src="{% static 'img/avatar.svg' %}" alt="{{ post.created_by.username }}" class="w-100">
    14. <small>Posts: {{ post.created_by.posts.count }}</small>
    15. </div>
    16. <div class="col-10">
    17. <div class="row mb-3">
    18. <div class="col-6">
    19. <strong class="text-muted">{{ post.created_by.username }}</strong>
    20. </div>
    21. <div class="col-6 text-right">
    22. <small class="text-muted">{{ post.created_at }}</small>
    23. </div>
    24. </div>
    25. {{ post.message }}
    26. {% if post.created_by == user %}
    27. <div class="mt-3">
    28. <a href="{% url 'edit_post' post.topic.board.pk post.topic.pk post.pk %}"
    29. class="btn btn-primary btn-sm"
    30. role="button">Edit</a>
    31. </div>
    32. {% endif %}
    33. </div>
    34. </div>
    35. </div>
    36. </div>
    37. {% endfor %}
    38. {% include 'includes/pagination.html' %}
    39. {% endblock %}

    别忘了修改主循环为 {% for post in posts %}

    我们同样也可以更新一下先前的模板,topics.html 模板同样也可以这个封装的分页模板。

    templates/topics.html 查看完整文件

    1. {% block content %}
    2. <div class="mb-4">
    3. <a href="{% url 'new_topic' board.pk %}" class="btn btn-primary">New topic</a>
    4. </div>
    5. <table class="table mb-4">
    6. <!-- table code suppressed -->
    7. </table>
    8. {% include 'includes/pagination.html' %}

    为了测试目的,你需要添加一些帖子(或者通过 python shell 去创建),然后修改代码中的 paginate_by 到一个较小的数字,比如 2 ,然后看看页面会发生什么变化。

    boards/tests/test_view_topic_posts.py