前言

欢迎来到系列教程的第六部分!在这篇教程中,我们将详细探讨基于类的视图(简称CBV)。我们也将重构一些现有的视图,以便利用内置的基于类的通用视图(Generic Class-Based Views)。

这篇教程我们还将讨论许多其他主题,例如如何使用分页,如何使用markdown以及如何添加简单的编辑器。我们还将探索一个名为 Humanize 的内置软件包,用于对数据进行“人为操作”。

好了,伙计们!让我们来实现一些代码。今天我们还有很多工作要做!


视图策略

到头来,所有的Django视图其实都是函数。即便是CBV。在类的底层,它完成了所有的功能并最终返回一个视图函数。

引入了基于类的视图,使开发人员可以更轻松地重新使用和扩展视图。使用它们有很多好处,例如可扩展性,多重继承之类的面向对象技术的运用,HTTP 方法的处理是在单独的方法中完成的,而不是使用条件分支,并且还有通用的基于类的视图(简称GCBV)。

在我们继续教程之前,让我们清楚这三个术语的含义:

  • 基于函数的视图(FBV)
  • 基于类的视图(CBV)
  • 基于类的通用视图(GCBV)

FBV是Django视图中最简单的表示:它就是一个接收 HttpRequest 对象并返回一个 HttpResponse 的函数。

CBV是每个Django视图都被定义为一个扩展 django.views.generic.view 抽象类后的 Python 类。

GCBV是可以解决一些具体问题的内置的CBV集合,比如列表视图,创建,更新和删除视图等。

下面我们将探讨一些不同实现方式的例子。

基于函数的视图 (FBV)

views.py

  1. def new_post(request):
  2. if request.method == 'POST':
  3. form = PostForm(request.POST)
  4. if form.is_valid():
  5. form.save()
  6. return redirect('post_list')
  7. else:
  8. form = PostForm()
  9. return render(request, 'new_post.html', {'form': form})

urls.py

  1. urlpatterns = [
  2. url(r'^new_post/$', views.new_post, name='new_post'),
  3. ]

基于类的视图 (CBV)

CBV是View类的集成类。这里的主要区别在于请求是以HTTP方法命名的类方法内处理的,例如GET ,POST ,PUT ,HEAD 等。

所以,在这里,我们不需要做一个条件来判断请求是一个 POST 还是它是一个 GET 。代码会直接跳转到正确的方法中。在 View 类中内部处理了这个逻辑。

views.py

  1. from django.views.generic import View
  2.  
  3. class NewPostView(View):
  4. def post(self, request):
  5. form = PostForm(request.POST)
  6. if form.is_valid():
  7. form.save()
  8. return redirect('post_list')
  9. return render(request, 'new_post.html', {'form': form})
  10.  
  11. def get(self, request):
  12. form = PostForm()
  13. return render(request, 'new_post.html', {'form': form})

我们在 urls.py 模块中引用CBV的方式也会有点不同:

urls.py

  1. urlpatterns = [
  2. url(r'^new_post/$', views.NewPostView.as_view(), name='new_post'),
  3. ]

在这里,我们需要调用 as_view() 这个类方法,它将返回一个符合url模式视图函数。在某些情况下,我们也可以将一些关键字参数传递给 as_view(),以便自定义CBV的行为,就像我们用一些身份验证视图来定制模板一样。

总之,关于CBV的好处是我们可以在类里面添加更多的方法,也许可以这样做:

  1. from django.views.generic import View
  2.  
  3. class NewPostView(View):
  4. def render(self, request):
  5. return render(request, 'new_post.html', {'form': self.form})
  6.  
  7. def post(self, request):
  8. self.form = PostForm(request.POST)
  9. if self.form.is_valid():
  10. self.form.save()
  11. return redirect('post_list')
  12. return self.render(request)
  13.  
  14. def get(self, request):
  15. self.form = PostForm()
  16. return self.render(request)

还可以创建一些通用视图来完成一些任务,以便我们可以在整个项目中重复利用它。

你需要知道的关于CBV的基本就是这些。就这么简单。

基于类的通过视图(GCBV)

接下来关于GCBV。这是一个不同的情况。正如我前面提到的,GCBV是内置CBV的常见用例。它们的实现往往会大量使用多重继承(混合继承)和其他面向对象的策略。

他们非常灵活,可以节省很多的工作量。但是一开始可能相对比较难上手。

当我第一次开始使用Django时,我发现GCBV很难使用。起初,很难说出发生了什么,因为代码执行流程看起来并不明显,因为在父类中隐藏了大量代码。官方文档也有一定的难度,主要是因为属性和方法有时分布在八个父类中。使用GCBV时,最好打开 ccbv.co.uk (需科学上网) 以便快速参考。不用担心,我们将一起探索它。

现在我们来看一个GCBV的例子。

views.py

  1. from django.views.generic import CreateView
  2.  
  3. class NewPostView(CreateView):
  4. model = Post
  5. form_class = PostForm
  6. success_url = reverse_lazy('post_list')
  7. template_name = 'new_post.html'

这里我们使用了一个用于创建模型对象的通用视图。它会处理所有表单处理并在表单有效时保存对象。

因为它是一个CBV,所以我们在 urls.py 中以与其他CBV相同的方式来引用它:

urls.py

  1. urlpatterns = [
  2. url(r'^new_post/$', views.NewPostView.as_view(), name='new_post'),
  3. ]

GCBV 中的其他例子还有:detailview,deleteview,formview,updateview,listview。


更新视图

让我们回到我们项目的实现。这次我们将使用 GCBV 来实现编辑帖子的视图:

boards/views.py (查看文件全部内容)

  1. from django.shortcuts import redirect
  2. from django.views.generic import UpdateView
  3. from django.utils import timezone
  4.  
  5. class PostUpdateView(UpdateView):
  6. model = Post
  7. fields = ('message', )
  8. template_name = 'edit_post.html'
  9. pk_url_kwarg = 'post_pk'
  10. context_object_name = 'post'
  11.  
  12. def form_valid(self, form):
  13. post = form.save(commit=False)
  14. post.updated_by = self.request.user
  15. post.updated_at = timezone.now()
  16. post.save()
  17. return redirect('topic_posts', pk=post.topic.board.pk, topic_pk=post.topic.pk)

使用 UPDATEVIEW 和 CREATEVIEW ,我们可以选择定义 form_class 或 fields 属性。在上面的例子中,我们使用 fields 属性来即时创建模型表单。在内部,Django 将使用模型表单工厂函数来组成 POST 模型的一种形式。因为它只是一个非常简单的表单,只有 message 字段,所以在这里我们可以这么做。但对于复杂的表单定义,最好从外部定义模型表单然后在这里引用它。

系统将使用 pk_url_kwarg 来标识用于检索 Post 对象的关键字参数的名称。就像我们在 urls.py 中定义一样。

如果我们没有设置 context_object_name 属性,Post 对象将作为“Object”在模板中可用。所以,在这里我们使用 context_object_name 来重命名它来发布。你会看到我们在下面的模板中如何使用它。

在这个特定的例子中,我们不得不重写 form_valid() 方法来设置一些额外的字段,例如updated_by 和 updated_at 。你可以在这里看到 form_valid() 方法原本的样子:updateview#form_valid

myproject/urls.py (查看文件全部内容)

  1. from django.conf.urls import url
  2. from boards import views
  3.  
  4. urlpatterns = [
  5. # ...
  6. url(r'^boards/(?P<pk>\d+)/topics/(?P<topic_pk>\d+)/posts/(?P<post_pk>\d+)/edit/$',
  7. views.PostUpdateView.as_view(), name='edit_post'),
  8. ]

现在我们可以将链接添加到编辑页面:

templates/topic_posts.html (查看文件全部内容)

  1. {% if post.created_by == user %}
  2. <div class="mt-3">
  3. <a href="{% url 'edit_post' post.topic.board.pk post.topic.pk post.pk %}"
  4. class="btn btn-primary btn-sm"
  5. role="button">Edit</a>
  6. </div>
  7. {% endif %}

templates/edit_post.html (查看文件全部内容)

  1. {% extends 'base.html' %}
  2.  
  3. {% block title %}Edit post{% endblock %}
  4.  
  5. {% block breadcrumb %}
  6. <li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li>
  7. <li class="breadcrumb-item"><a href="{% url 'board_topics' post.topic.board.pk %}">{{ post.topic.board.name }}</a></li>
  8. <li class="breadcrumb-item"><a href="{% url 'topic_posts' post.topic.board.pk post.topic.pk %}">{{ post.topic.subject }}</a></li>
  9. <li class="breadcrumb-item active">Edit post</li>
  10. {% endblock %}
  11.  
  12. {% block content %}
  13. <form method="post" class="mb-4" novalidate>
  14. {% csrf_token %}
  15. {% include 'includes/form.html' %}
  16. <button type="submit" class="btn btn-success">Save changes</button>
  17. <a href="{% url 'topic_posts' post.topic.board.pk post.topic.pk %}" class="btn btn-outline-secondary" role="button">Cancel</a>
  18. </form>
  19. {% endblock %}

现在观察我们如何引导到 post 对象:post.topic.board.pk。如果我们没有设置context_object_name 来发布,它将可以被用作:object.topic.board.pk。明白了吗?

测试更新视图

在 boards/tests 文件夹内创建一个名为 test_view_edit_post.py 的新测试文件。点击下面的链接,你会看到很多常用测试,就像我们在本教程中做的一样。我会在这里重点介绍一下新的内容:

boards/tests/test_view_edit_post.py (查看完整文件)

  1. from django.contrib.auth.models import User
  2. from django.test import TestCase
  3. from django.urls import reverse
  4. from ..models import Board, Post, Topic
  5. from ..views import PostUpdateView
  6.  
  7. class PostUpdateViewTestCase(TestCase):
  8. '''
  9. Base test case to be used in all `PostUpdateView` view tests
  10. '''
  11. def setUp(self):
  12. self.board = Board.objects.create(name='Django', description='Django board.')
  13. self.username = 'john'
  14. self.password = '123'
  15. user = User.objects.create_user(username=self.username, email='john@doe.com', password=self.password)
  16. self.topic = Topic.objects.create(subject='Hello, world', board=self.board, starter=user)
  17. self.post = Post.objects.create(message='Lorem ipsum dolor sit amet', topic=self.topic, created_by=user)
  18. self.url = reverse('edit_post', kwargs={
  19. 'pk': self.board.pk,
  20. 'topic_pk': self.topic.pk,
  21. 'post_pk': self.post.pk
  22. })
  23.  
  24. class LoginRequiredPostUpdateViewTests(PostUpdateViewTestCase):
  25. def test_redirection(self):
  26. '''
  27. Test if only logged in users can edit the posts
  28. '''
  29. login_url = reverse('login')
  30. response = self.client.get(self.url)
  31. self.assertRedirects(response, '{login_url}?next={url}'.format(login_url=login_url, url=self.url))
  32.  
  33. class UnauthorizedPostUpdateViewTests(PostUpdateViewTestCase):
  34. def setUp(self):
  35. '''
  36. Create a new user different from the one who posted
  37. '''
  38. super().setUp()
  39. username = 'jane'
  40. password = '321'
  41. user = User.objects.create_user(username=username, email='jane@doe.com', password=password)
  42. self.client.login(username=username, password=password)
  43. self.response = self.client.get(self.url)
  44.  
  45. def test_status_code(self):
  46. '''
  47. A topic should be edited only by the owner.
  48. Unauthorized users should get a 404 response (Page Not Found)
  49. '''
  50. self.assertEquals(self.response.status_code, 404)
  51.  
  52. class PostUpdateViewTests(PostUpdateViewTestCase):
  53. # ...
  54.  
  55. class SuccessfulPostUpdateViewTests(PostUpdateViewTestCase):
  56. # ...
  57.  
  58. class InvalidPostUpdateViewTests(PostUpdateViewTestCase):
  59. # ...

这里,重要的部分是:PostUpdateViewTestCase是我们定义的类,它可以在其他测试用例中复用。它只包含基本的设置,创建user,topic,boards等等。

LoginRequiredPostUpdateViewTests 这个类将测试检查该视图是否使用了@login_required装饰器。即只有经过身份验证的用户才能访问编辑页面。

UnauthorizedPostUpdateViewTests 这个类是创建一个新用户,与发布并尝试访问编辑页面的用户不同。本应用程序应该只能授权该文章的所有者进行编辑。

我们来运行一下这些测试:

  1. python manage.py test boards.tests.test_view_edit_post
  1. Creating test database for alias 'default'...
  2. System check identified no issues (0 silenced).
  3. ..F.......F
  4. ======================================================================
  5. FAIL: test_redirection (boards.tests.test_view_edit_post.LoginRequiredPostUpdateViewTests)
  6. ----------------------------------------------------------------------
  7. ...
  8. AssertionError: 200 != 302 : Response didn't redirect as expected: Response code was 200 (expected 302)
  9.  
  10. ======================================================================
  11. FAIL: test_status_code (boards.tests.test_view_edit_post.UnauthorizedPostUpdateViewTests)
  12. ----------------------------------------------------------------------
  13. ...
  14. AssertionError: 200 != 404
  15.  
  16. ----------------------------------------------------------------------
  17. Ran 11 tests in 1.360s
  18.  
  19. FAILED (failures=2)
  20. Destroying test database for alias 'default'...

首先,我们修复@login_required装饰器的问题。在 CBV 上使用视图装饰器的方式有一些不同。我们需要额外的导入:

boards/views.py (查看文章文件)

  1. from django.contrib.auth.decorators import login_required
  2. from django.shortcuts import redirect
  3. from django.views.generic import UpdateView
  4. from django.utils import timezone
  5. from django.utils.decorators import method_decorator
  6. from .models import Post
  7.  
  8. @method_decorator(login_required, name='dispatch')
  9. class PostUpdateView(UpdateView):
  10. model = Post
  11. fields = ('message', )
  12. template_name = 'edit_post.html'
  13. pk_url_kwarg = 'post_pk'
  14. context_object_name = 'post'
  15.  
  16. def form_valid(self, form):
  17. post = form.save(commit=False)
  18. post.updated_by = self.request.user
  19. post.updated_at = timezone.now()
  20. post.save()
  21. return redirect('topic_posts', pk=post.topic.board.pk, topic_pk=post.topic.pk)

我们不能用 @login_required 装饰器直接装饰类。我们必须使用一个工具@method_decorator,并传递一个装饰器(或一个装饰器列表)并告诉应该装饰哪个类。在 CBV 中,装饰调度类是很常见的。它是一个Django内部使用的方法(在View类中定义)。所有的请求都会经过这个类,所以装饰它会相对安全。

再次运行一下测试:

  1. python manage.py test boards.tests.test_view_edit_post
  1. Creating test database for alias 'default'...
  2. System check identified no issues (0 silenced).
  3. ..........F
  4. ======================================================================
  5. FAIL: test_status_code (boards.tests.test_view_edit_post.UnauthorizedPostUpdateViewTests)
  6. ----------------------------------------------------------------------
  7. ...
  8. AssertionError: 200 != 404
  9.  
  10. ----------------------------------------------------------------------
  11. Ran 11 tests in 1.353s
  12.  
  13. FAILED (failures=1)
  14. Destroying test database for alias 'default'...

好的,我们解决了 @login_required 的问题,现在我们必须处理其他用户可以编辑所有帖子的问题。

解决这个问题最简单的方法是重写UpdateView的 get_queryset 方法。你可以在这里看到原始方法的源码--- UpdateView#get_queryset.

boards/views.py 查看完整文件

  1. @method_decorator(login_required, name='dispatch')
  2. class PostUpdateView(UpdateView):
  3. model = Post
  4. fields = ('message', )
  5. template_name = 'edit_post.html'
  6. pk_url_kwarg = 'post_pk'
  7. context_object_name = 'post'
  8.  
  9. def get_queryset(self):
  10. queryset = super().get_queryset()
  11. return queryset.filter(created_by=self.request.user)
  12.  
  13. def form_valid(self, form):
  14. post = form.save(commit=False)
  15. post.updated_by = self.request.user
  16. post.updated_at = timezone.now()
  17. post.save()
  18. return redirect('topic_posts', pk=post.topic.board.pk, topic_pk=post.topic.pk)

通过这一行 queryset = super().get_queryset(), 我们实现了重用父类,即,UpateView 类的 get_queryset 方法。然后,我们通过给 queryset 添加一个额外的过滤条件,该过滤条件是通过请求中获取登录的用户来过滤内容。

再次测试:

  1. python manage.py test boards.tests.test_view_edit_post
  1. Creating test database for alias 'default'...
  2. System check identified no issues (0 silenced).
  3. ...........
  4. ----------------------------------------------------------------------
  5. Ran 11 tests in 1.321s
  6.  
  7. OK
  8. Destroying test database for alias 'default'...

一切顺利!


列表视图

我们现在可以利用 CBV 的功能来重构一些现有的视图。以主页为例,我们就先从数据库中抓取所有的 boards 并将其罗列在HTML中:

boards/views.py

  1. from django.shortcuts import render
  2. from .models import Board
  3.  
  4. def home(request):
  5. boards = Board.objects.all()
  6. return render(request, 'home.html', {'boards': boards})

下面是我们如何使用 GCBV 为模型列表来重写它:

boards/views.py 查看完整文件

  1. from django.views.generic import ListView
  2. from .models import Board
  3.  
  4. class BoardListView(ListView):
  5. model = Board
  6. context_object_name = 'boards'
  7. template_name = 'home.html'

那么我们得修改一下 urls.py 模块中的引用:

myproject/urls.py 查看完整文件

  1. from django.conf.urls import url
  2. from boards import views
  3.  
  4. urlpatterns = [
  5. url(r'^$', views.BoardListView.as_view(), name='home'),
  6. # ...
  7. ]

如果检查一下主页,我们会看到没有什么变化,一切都按预期的运行。但是我们必须稍微调整我们的测试,因为现在我们现在的视图是 CBV 类型了。

boards/tests/test_view_home.py 查看完整文件

  1. from django.test import TestCase
  2. from django.urls import resolve
  3. from ..views import BoardListView
  4.  
  5. class HomeTests(TestCase):
  6. # ...
  7. def test_home_url_resolves_home_view(self):
  8. view = resolve('/')
  9. self.assertEquals(view.func.view_class, BoardListView)

分页

我们可以非常容易地使用 CBV 来实现分页功能。但首先我想手工分页,这样就更有助于我们理解背后的机制,这样它就不那么神秘了。

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

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

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

  1. python manage.py shell
  1. from django.contrib.auth.models import User
  2. from boards.models import Board, Topic, Post
  3.  
  4. user = User.objects.first()
  5.  
  6. board = Board.objects.get(name='Django')
  7.  
  8. for i in range(100):
  9. subject = 'Topic test #{}'.format(i)
  10. topic = Topic.objects.create(subject=subject, board=board, starter=user)
  11. 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.  
  3. # All the topics in the app
  4. Topic.objects.count()
  5. 107
  6.  
  7. # Just the topics in the Django board
  8. Topic.objects.filter(board__name='Django').count()
  9. 104
  10.  
  11. # Let's save this queryset into a variable to paginate it
  12. queryset = Topic.objects.filter(board__name='Django').order_by('-last_updated')

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

现在让我们导入 Paginator 工具:

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

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

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

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

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

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

  1. paginator.page('abc')
  2. PageNotAnInteger: That page number is not an integer

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

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

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

FBV 分页

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

boards/views.py 查看完整文件

  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.  
  7. def board_topics(request, pk):
  8. board = get_object_or_404(Board, pk=pk)
  9. queryset = board.topics.order_by('-last_updated').annotate(replies=Count('posts') - 1)
  10. page = request.GET.get('page', 1)
  11.  
  12. paginator = Paginator(queryset, 20)
  13.  
  14. try:
  15. topics = paginator.page(page)
  16. except PageNotAnInteger:
  17. # fallback to the first page
  18. topics = paginator.page(1)
  19. except EmptyPage:
  20. # probably the user tried to add a page number
  21. # in the url, so we fallback to the last page
  22. topics = paginator.page(paginator.num_pages)
  23.  
  24. 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.  
  14. {% for page_num in topics.paginator.page_range %}
  15. {% if topics.number == page_num %}
  16. <li class="page-item active">
  17. <span class="page-link">
  18. {{ page_num }}
  19. <span class="sr-only">(current)</span>
  20. </span>
  21. </li>
  22. {% else %}
  23. <li class="page-item">
  24. <a class="page-link" href="?page={{ page_num }}">{{ page_num }}</a>
  25. </li>
  26. {% endif %}
  27. {% endfor %}
  28.  
  29. {% if topics.has_next %}
  30. <li class="page-item">
  31. <a class="page-link" href="?page={{ topics.next_page_number }}">Next</a>
  32. </li>
  33. {% else %}
  34. <li class="page-item disabled">
  35. <span class="page-link">Next</span>
  36. </li>
  37. {% endif %}
  38. </ul>
  39. </nav>
  40. {% endif %}

GCBV 分页

下面,相同的实现,但这次使用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.  
  7. def get_context_data(self, **kwargs):
  8. kwargs['board'] = self.board
  9. return super().get_context_data(**kwargs)
  10.  
  11. def get_queryset(self):
  12. self.board = get_object_or_404(Board, pk=self.kwargs.get('pk'))
  13. queryset = self.board.topics.order_by('-last_updated').annotate(replies=Count('posts') - 1)
  14. 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.  
  4. urlpatterns = [
  5. # ...
  6. url(r'^boards/(?P<pk>\d+)/$', views.TopicListView.as_view(), name='board_topics'),
  7. ]

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

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

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

boards/tests/test_view_board_topics.py

  1. from django.test import TestCase
  2. from django.urls import resolve
  3. from ..views import TopicListView
  4.  
  5. class BoardTopicsTests(TestCase):
  6. # ...
  7. def test_board_topics_url_resolves_board_topics_view(self):
  8. view = resolve('/boards/1/')
  9. self.assertEquals(view.func.view_class, TopicListView)

可复用的分页模板

就像我们在 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.  
  7. def get_context_data(self, **kwargs):
  8. self.topic.views += 1
  9. self.topic.save()
  10. kwargs['topic'] = self.topic
  11. return super().get_context_data(**kwargs)
  12.  
  13. def get_queryset(self):
  14. self.topic = get_object_or_404(Topic, board__pk=self.kwargs.get('pk'), pk=self.kwargs.get('topic_pk'))
  15. queryset = self.topic.posts.order_by('created_at')
  16. return queryset

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

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

现在,我们从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.  
  14. {% for page_num in paginator.page_range %}
  15. {% if page_obj.number == page_num %}
  16. <li class="page-item active">
  17. <span class="page-link">
  18. {{ page_num }}
  19. <span class="sr-only">(current)</span>
  20. </span>
  21. </li>
  22. {% else %}
  23. <li class="page-item">
  24. <a class="page-link" href="?page={{ page_num }}">{{ page_num }}</a>
  25. </li>
  26. {% endif %}
  27. {% endfor %}
  28.  
  29. {% if page_obj.has_next %}
  30. <li class="page-item">
  31. <a class="page-link" href="?page={{ page_obj.next_page_number }}">Next</a>
  32. </li>
  33. {% else %}
  34. <li class="page-item disabled">
  35. <span class="page-link">Next</span>
  36. </li>
  37. {% endif %}
  38. </ul>
  39. </nav>
  40. {% endif %}

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

templates/topic_posts.html 查看完整文件

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

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

(查看完整文件)

更新一下测试用例:

boards/tests/test_view_topic_posts.py

  1. from django.test import TestCase
  2. from django.urls import resolve
  3. from ..views import PostListView
  4.  
  5. class TopicPostsTests(TestCase):
  6. # ...
  7. def test_view_function(self):
  8. view = resolve('/boards/1/topics/1/')
  9. self.assertEquals(view.func.view_class, PostListView)

我的账户视图

好的,那么,这部分将是我们最后的一个视图。之后,我们将专心来改进现有功能。

accounts/views.py 查看完整文件

  1. from django.contrib.auth.decorators import login_required
  2. from django.contrib.auth.models import User
  3. from django.urls import reverse_lazy
  4. from django.utils.decorators import method_decorator
  5. from django.views.generic import UpdateView
  6.  
  7. @method_decorator(login_required, name='dispatch')
  8. class UserUpdateView(UpdateView):
  9. model = User
  10. fields = ('first_name', 'last_name', 'email', )
  11. template_name = 'my_account.html'
  12. success_url = reverse_lazy('my_account')
  13.  
  14. def get_object(self):
  15. return self.request.user

myproject/urls.py 查看完整文件

  1. from django.conf.urls import url
  2. from accounts import views as accounts_views
  3.  
  4. urlpatterns = [
  5. # ...
  6. url(r'^settings/account/$', accounts_views.UserUpdateView.as_view(), name='my_account'),
  7. ]

templates/my_account.html

  1. {% extends 'base.html' %}
  2.  
  3. {% block title %}My account{% endblock %}
  4.  
  5. {% block breadcrumb %}
  6. <li class="breadcrumb-item active">My account</li>
  7. {% endblock %}
  8.  
  9. {% block content %}
  10. <div class="row">
  11. <div class="col-lg-6 col-md-8 col-sm-10">
  12. <form method="post" novalidate>
  13. {% csrf_token %}
  14. {% include 'includes/form.html' %}
  15. <button type="submit" class="btn btn-success">Save changes</button>
  16. </form>
  17. </div>
  18. </div>
  19. {% endblock %}


添加 Markdown 支持

让我们在文本区域添加 Markdown 支持来改善用户体验。你会看到要实现这个功能非常简单。

首先,我们安装一个名为 Python-Markdown 的库:

  1. pip install markdown

我们可以在 Post 视图的 model 中添加一个新的方法:

boards/models.py 查看完整文件

  1. from django.db import models
  2. from django.utils.html import mark_safe
  3. from markdown import markdown
  4.  
  5. class Post(models.Model):
  6. # ...
  7.  
  8. def get_message_as_markdown(self):
  9. return mark_safe(markdown(self.message, safe_mode='escape'))

这里我们正在处理用户的输入,所以我们需要小心一点。当使用 Mardown 功能时,我们需要先让它转义一下特殊字符,然后再解析出 Markdown 标签。这样做之后,输出字符串可以安全的在模板中使用。

现在,我们只需要在模板 topic_posts.html 和 reply_topic.html 中修改一下 form。

  1. {{ post.message }}

修改为:

  1. {{ post.get_message_as_markdown }}

从现在起,用户就可以在帖子中使用 Mardown 语法来编辑了。

Markdown 编辑器

我们还可以添加一个名为 SimpleMD 的非常酷的 Markdown 编辑器。

可以下载 JavaScript 库,后者使用他们的CDN:

  1. <link rel="stylesheet" href="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css">
  2. <script src="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script>

现在来编辑一下 base.html ,为这些额外的Javascripts声明一个block (译者注:方便其他模板继承):

templates/base.html 查看完整文件

  1. <script src="{% static 'js/jquery-3.2.1.min.js' %}"></script>
  2. <script src="{% static 'js/popper.min.js' %}"></script>
  3. <script src="{% static 'js/bootstrap.min.js' %}"></script>
  4. {% block javascript %}{% endblock %} <!-- Add this empty block here! -->

首先来编辑 reply_topic.html 模板:

templates/reply_topic.html 查看完整文件

  1. {% extends 'base.html' %}
  2.  
  3. {% load static %}
  4.  
  5. {% block title %}Post a reply{% endblock %}
  6.  
  7. {% block stylesheet %}
  8. <link rel="stylesheet" href="{% static 'css/simplemde.min.css' %}">
  9. {% endblock %}
  10.  
  11. {% block javascript %}
  12. <script src="{% static 'js/simplemde.min.js' %}"></script>
  13. <script>
  14. var simplemde = new SimpleMDE();
  15. </script>
  16. {% endblock %}

默认情况下,这个插件会将它找到的第一个文本区域转换为 markdown 编辑器。所以这点代码应该就足够了:

接下来在 edit_post.html 模板中做同样的操作:

templates/edit_post.html 查看完整文件

  1. {% extends 'base.html' %}
  2.  
  3. {% load static %}
  4.  
  5. {% block title %}Edit post{% endblock %}
  6.  
  7. {% block stylesheet %}
  8. <link rel="stylesheet" href="{% static 'css/simplemde.min.css' %}">
  9. {% endblock %}
  10.  
  11. {% block javascript %}
  12. <script src="{% static 'js/simplemde.min.js' %}"></script>
  13. <script>
  14. var simplemde = new SimpleMDE();
  15. </script>
  16. {% endblock %}


人性化

我觉得只添加内置的人性化(humanize)包就会很不错。它包含一组为数据添加“人性化(human touch)”的工具集。

例如,我们可以使用它来更自然地显示日期和时间字段。我们可以简单地显示:“2分钟前”,而不是显示整个日期。

我们来实践一下!首先,添加 django.contrib.humanize 到配置文件的 INSTALLED_APPS 中。

myproject/settings.py

  1. INSTALLED_APPS = [
  2. 'django.contrib.admin',
  3. 'django.contrib.auth',
  4. 'django.contrib.contenttypes',
  5. 'django.contrib.sessions',
  6. 'django.contrib.messages',
  7. 'django.contrib.staticfiles',
  8. 'django.contrib.humanize', # <- 这里
  9.  
  10. 'widget_tweaks',
  11.  
  12. 'accounts',
  13. 'boards',
  14. ]

现在我们就可以在模板中使用它了。首先来编辑 topics.html 模板:

templates/topics.html 查看完整文件

  1. {% extends 'base.html' %}
  2.  
  3. {% load humanize %}
  4.  
  5. {% block content %}
  6. <!-- 代码被压缩 -->
  7.  
  8. <td>{{ topic.last_updated|naturaltime }}</td>
  9.  
  10. <!-- 代码被压缩 -->
  11. {% endblock %}

我们所要做的就是在模板中加载 {%load humanize%} 这个模板标签,然后在模板中使用过滤器: {{ topic.last_updated|naturaltime }}

你当然可以将它添加到其他你需要的地方。


Gravatar(添加头像用的库)

给用户个人信息添加图片的一种非常简单的方法就是使用 Gravatar

在 boards/templatetags 文件夹内,创建一个名为 gravatar.py 的新文件:

boards/templatetags/gravatar.py

  1. import hashlib
  2. from urllib.parse import urlencode
  3.  
  4. from django import template
  5. from django.conf import settings
  6.  
  7. register = template.Library()
  8.  
  9. @register.filter
  10. def gravatar(user):
  11. email = user.email.lower().encode('utf-8')
  12. default = 'mm'
  13. size = 256
  14. url = 'https://www.gravatar.com/avatar/{md5}?{params}'.format(
  15. md5=hashlib.md5(email).hexdigest(),
  16. params=urlencode({'d': default, 's': str(size)})
  17. )
  18. return url

基本上我们可以使用官方提供的代码片段。我只是做了一下适配,使得它可以在python 3环境中运行。

很好,现在我们可以将它加载到我们的模板中,就像之前我们使用人性化模板过滤器一样:

templates/topic_posts.html 查看完整文件

  1. {% extends 'base.html' %}
  2.  
  3. {% load gravatar %}
  4.  
  5. {% block content %}
  6. <!-- code suppressed -->
  7.  
  8. <img src="{{ post.created_by|gravatar }}" alt="{{ post.created_by.username }}" class="w-100 rounded">
  9.  
  10. <!-- code suppressed -->
  11. {% endblock %}


最后调整

也许你已经注意到了,如果有人回复帖子时有一个小问题。我们没有更新 last_update 字段,因此主题的排序被打乱顺序了。

我们来修一下:

boards/views.py

  1. @login_required
  2. def reply_topic(request, pk, topic_pk):
  3. topic = get_object_or_404(Topic, board__pk=pk, pk=topic_pk)
  4. if request.method == 'POST':
  5. form = PostForm(request.POST)
  6. if form.is_valid():
  7. post = form.save(commit=False)
  8. post.topic = topic
  9. post.created_by = request.user
  10. post.save()
  11.  
  12. topic.last_updated = timezone.now() # <- 这里
  13. topic.save() # <- 这里
  14.  
  15. return redirect('topic_posts', pk=pk, topic_pk=topic_pk)
  16. else:
  17. form = PostForm()
  18. return render(request, 'reply_topic.html', {'topic': topic, 'form': form})

接下来我们要做的事是需要控制一下页面访问统计系统。我们不希望相同的用户再次刷新页面的时候被统计为多次访问。为此,我们可以使用会话(sessions):

boards/views.py

  1. class PostListView(ListView):
  2. model = Post
  3. context_object_name = 'posts'
  4. template_name = 'topic_posts.html'
  5. paginate_by = 20
  6.  
  7. def get_context_data(self, **kwargs):
  8.  
  9. session_key = 'viewed_topic_{}'.format(self.topic.pk) # <--这里
  10. if not self.request.session.get(session_key, False):
  11. self.topic.views += 1
  12. self.topic.save()
  13. self.request.session[session_key] = True # <--直到这里
  14.  
  15. kwargs['topic'] = self.topic
  16. return super().get_context_data(**kwargs)
  17.  
  18. def get_queryset(self):
  19. self.topic = get_object_or_404(Topic, board__pk=self.kwargs.get('pk'), pk=self.kwargs.get('topic_pk'))
  20. queryset = self.topic.posts.order_by('created_at')
  21. return queryset

现在我们可以在主题列表中提供一个更好一点的导航。目前唯一的选择是用户点击主题标题并转到第一页。我们可以实践一下这么做:

boards/models.py

  1. import math
  2. from django.db import models
  3.  
  4. class Topic(models.Model):
  5. # ...
  6.  
  7. def __str__(self):
  8. return self.subject
  9.  
  10. def get_page_count(self):
  11. count = self.posts.count()
  12. pages = count / 20
  13. return math.ceil(pages)
  14.  
  15. def has_many_pages(self, count=None):
  16. if count is None:
  17. count = self.get_page_count()
  18. return count > 6
  19.  
  20. def get_page_range(self):
  21. count = self.get_page_count()
  22. if self.has_many_pages(count):
  23. return range(1, 5)
  24. return range(1, count + 1)

然后,在 topics.html 模板中,我们可以这样实现:

templates/topics.html

  1. <table class="table table-striped mb-4">
  2. <thead class="thead-inverse">
  3. <tr>
  4. <th>Topic</th>
  5. <th>Starter</th>
  6. <th>Replies</th>
  7. <th>Views</th>
  8. <th>Last Update</th>
  9. </tr>
  10. </thead>
  11. <tbody>
  12. {% for topic in topics %}
  13. {% url 'topic_posts' board.pk topic.pk as topic_url %}
  14. <tr>
  15. <td>
  16. <p class="mb-0">
  17. <a href="{{ topic_url }}">{{ topic.subject }}</a>
  18. </p>
  19. <small class="text-muted">
  20. Pages:
  21. {% for i in topic.get_page_range %}
  22. <a href="{{ topic_url }}?page={{ i }}">{{ i }}</a>
  23. {% endfor %}
  24. {% if topic.has_many_pages %}
  25. ... <a href="{{ topic_url }}?page={{ topic.get_page_count }}">Last Page</a>
  26. {% endif %}
  27. </small>
  28. </td>
  29. <td class="align-middle">{{ topic.starter.username }}</td>
  30. <td class="align-middle">{{ topic.replies }}</td>
  31. <td class="align-middle">{{ topic.views }}</td>
  32. <td class="align-middle">{{ topic.last_updated|naturaltime }}</td>
  33. </tr>
  34. {% endfor %}
  35. </tbody>
  36. </table>

就像每个主题的小分页一样。请注意,我在 table 标签里还添加了 table-striped 类,使得表格有一个更好的样式。

在回复页面中,我们现在是列出了所有的回复。我们可以将它限制在最近的十个回复。

boards/models.py

  1. class Topic(models.Model):
  2. # ...
  3.  
  4. def get_last_ten_posts(self):
  5. return self.posts.order_by('-created_at')[:10]

templates/reply_topic.html

  1. {% block content %}
  2.  
  3. <form method="post" class="mb-4" novalidate>
  4. {% csrf_token %}
  5. {% include 'includes/form.html' %}
  6. <button type="submit" class="btn btn-success">Post a reply</button>
  7. </form>
  8.  
  9. {% for post in topic.get_last_ten_posts %} <!-- here! -->
  10. <div class="card mb-2">
  11. <!-- code suppressed -->
  12. </div>
  13. {% endfor %}
  14.  
  15. {% endblock %}

另一件事是,当用户回复帖子时,我们现在是会再次将用户重定向到第一页。我们可以通过将用户送回到最后一页来改善这个问题。

我们可以在帖子上添加一个ID:

templates/topic_posts.html

  1. {% block content %}
  2.  
  3. <div class="mb-4">
  4. <a href="{% url 'reply_topic' topic.board.pk topic.pk %}" class="btn btn-primary" role="button">Reply</a>
  5. </div>
  6.  
  7. {% for post in posts %}
  8. <div id="{{ post.pk }}" class="card {% if forloop.last %}mb-4{% else %}mb-2{% endif %} {% if forloop.first %}border-dark{% endif %}">
  9. <!-- code suppressed -->
  10. </div>
  11. {% endfor %}
  12.  
  13. {% include 'includes/pagination.html' %}
  14.  
  15. {% endblock %}

这里的重要点是 <div id="{{ post.pk }}" ...>

然后我们可以在视图中像这样使用它:

boards/views.py

  1. @login_required
  2. def reply_topic(request, pk, topic_pk):
  3. topic = get_object_or_404(Topic, board__pk=pk, pk=topic_pk)
  4. if request.method == 'POST':
  5. form = PostForm(request.POST)
  6. if form.is_valid():
  7. post = form.save(commit=False)
  8. post.topic = topic
  9. post.created_by = request.user
  10. post.save()
  11.  
  12. topic.last_updated = timezone.now()
  13. topic.save()
  14.  
  15. topic_url = reverse('topic_posts', kwargs={'pk': pk, 'topic_pk': topic_pk})
  16. topic_post_url = '{url}?page={page}#{id}'.format(
  17. url=topic_url,
  18. id=post.pk,
  19. page=topic.get_page_count()
  20. )
  21.  
  22. return redirect(topic_post_url)
  23. else:
  24. form = PostForm()
  25. return render(request, 'reply_topic.html', {'topic': topic, 'form': form})

在 topic_post_url 中,我们使用最后一页来构建一个url,添加一个锚点id等于帖子id的元素。

有了这个,这要求我们需要更新下面的这些测试用例:

boards/tests/test_view_reply_topic.py

  1. class SuccessfulReplyTopicTests(ReplyTopicTestCase):
  2. # ...
  3.  
  4. def test_redirection(self):
  5. '''
  6. A valid form submission should redirect the user
  7. '''
  8. url = reverse('topic_posts', kwargs={'pk': self.board.pk, 'topic_pk': self.topic.pk})
  9. topic_posts_url = '{url}?page=1#2'.format(url=url)
  10. self.assertRedirects(self.response, topic_posts_url)

下一个问题,正如你在前面的截图中看到的,要解决分页时页数太多的问题。

最简单的方法是调整 pagination.html 模板:

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.number > 1 %}
  5. <li class="page-item">
  6. <a class="page-link" href="?page=1">First</a>
  7. </li>
  8. {% else %}
  9. <li class="page-item disabled">
  10. <span class="page-link">First</span>
  11. </li>
  12. {% endif %}
  13.  
  14. {% if page_obj.has_previous %}
  15. <li class="page-item">
  16. <a class="page-link" href="?page={{ page_obj.previous_page_number }}">Previous</a>
  17. </li>
  18. {% else %}
  19. <li class="page-item disabled">
  20. <span class="page-link">Previous</span>
  21. </li>
  22. {% endif %}
  23.  
  24. {% for page_num in paginator.page_range %}
  25. {% if page_obj.number == page_num %}
  26. <li class="page-item active">
  27. <span class="page-link">
  28. {{ page_num }}
  29. <span class="sr-only">(current)</span>
  30. </span>
  31. </li>
  32. {% elif page_num > page_obj.number|add:'-3' and page_num < page_obj.number|add:'3' %}
  33. <li class="page-item">
  34. <a class="page-link" href="?page={{ page_num }}">{{ page_num }}</a>
  35. </li>
  36. {% endif %}
  37. {% endfor %}
  38.  
  39. {% if page_obj.has_next %}
  40. <li class="page-item">
  41. <a class="page-link" href="?page={{ page_obj.next_page_number }}">Next</a>
  42. </li>
  43. {% else %}
  44. <li class="page-item disabled">
  45. <span class="page-link">Next</span>
  46. </li>
  47. {% endif %}
  48.  
  49. {% if page_obj.number != paginator.num_pages %}
  50. <li class="page-item">
  51. <a class="page-link" href="?page={{ paginator.num_pages }}">Last</a>
  52. </li>
  53. {% else %}
  54. <li class="page-item disabled">
  55. <span class="page-link">Last</span>
  56. </li>
  57. {% endif %}
  58. </ul>
  59. </nav>
  60. {% endif %}


总结

在本教程中,我们完成了Django board项目应用的实现。我可能会发布一个后续的实现教程来改进代码。我们可以一起研究很多事情。例如数据库优化,改进用户界面,文件上传操作,创建审核系统等等。

下一篇教程将着重于部署。它将是关于如何将你的代码投入到生产中以及需要关注的一些重要细节的完整指南。

我希望你会喜欢本系列教程的第六部分!最后一部分将于下周2017年10月16日发布。如果你希望在最后一部分发布时收到通知,可以订阅我们的邮件列表

该项目的源代码在github上找到。当前状态的该项目的可以在发布标签 v0.6-lw 下找到。或者直接点击下面的链接:

https://github.com/sibtc/django-beginners-guide/tree/v0.6-lw

完整的Django入门指南学习笔记6的更多相关文章

  1. 完整的Django入门指南学习笔记7 网页自动翻译

    转自[https://simpleisbetterthancomplex.com/series/2017/10/16/a-complete-beginners-guide-to-django-part ...

  2. 完整的Django入门指南学习笔记5

    前言 欢迎来到本系列教程的第5部分,在这节课,我们将学习如何保护视图防止未登录的用户访问,以及在视图和表单中访问已经登录的用户,我们还将实现主题列表和回复列表视图,最后,将探索Django ORM的一 ...

  3. 完整的Django入门指南学习笔记2

    part2: 前沿 在第一节中,我们安装了项目所需要的一切:Python3.6以及在虚拟环境中运行的Django2.0,这部分教程继续在项目上编写代码. 开始写代码前,先讨论下项目的相关背景知识,然后 ...

  4. 完整的Django入门指南学习笔记1

    转自[https://blog.csdn.net/qq_35554125/article/details/79462885] part 1: 前沿 教程材料一共会被分为七个部分. 此教程将从安装.开发 ...

  5. 完整的Django入门指南学习笔记4

    前言 这一章节将会全面介绍 Django 的身份认证系统,我们将实现注册.登录.注销.密码重置和密码修改的整套流程. 同时你还会了解到如何保护某些试图以防未授权的用户访问,以及如何访问已登录用户的个人 ...

  6. 完整的Django入门指南学习笔记3

    前言 在本节课中,我们将深入理解两个基本概念: URLs 和 Forms.在这个过程中,我们还将学习其它很多概念,如创建可重用模板和安装第三方库.同时我们还将编写大量单元测试. 如果你是从这个系列教程 ...

  7. redis入门指南学习笔记

    redis的常见命令 set key hello get key incr num get num set foo lorem incr foo hset car price 500 hset car ...

  8. Webpack新手入门教程(学习笔记)

    p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; text-align: center; font: 30.0px Helvetica; color: #000000 } ...

  9. Django RF:学习笔记(8)——快速开始

    Django RF:学习笔记(8)——快速开始 安装配置 1.使用Pip安装Django REST Framework: pip install djangorestframework 2.在Sett ...

随机推荐

  1. 《Redis 优化》

    一:管道技术 - 由于 redis 和 客户端是使用 TCP 连接的,那么在使用中就会产生往返耗时. - 虽然可能单条影响并不大,但是如果执行较多的命令会对性能产生影响. - 使用管道原理和 keep ...

  2. ConnectionAbortedError: [WinError 10053] 你的主机中的软件中止了一个已建立的连接

    Traceback (most recent call last): File "C:\Users\Administrator\AppData\Local\Programs\Python\P ...

  3. Mac 启动或者禁用翻盖自动开关机

    从 2016 新款 MacBook Pro 起,预设打开笔电上盖或连接电源供应器时,电脑就会自动开机而且开机时没有启动声,本文教你如何将这些东西调整回原本的样子. 以下指令都是通过「应用程序」→「终端 ...

  4. PTA地下迷宫探索

    地道战是在抗日战争时期,在华北平原上抗日军民利用地道打击日本侵略者的作战方式.地道网是房连房.街连街.村连村的地下工事,如下图所示. 我们在回顾前辈们艰苦卓绝的战争生活的同时,真心钦佩他们的聪明才智. ...

  5. python3 使用libvirt 相关安装

    1.Linux下有Python2的环境下安装Python3参考: https://www.cnblogs.com/kimyeee/p/7250560.html 2.安装完Python3后,将/usr/ ...

  6. linux出现tmp空间满的情况解决

    cd命令tab补全的时候报错: cd /ro-bash: cannot create temp file for here-document: No space left on device-bash ...

  7. 基于UVM的verilog验证

    Abstract 本文介绍UVM框架,并以crc7为例进行UVM的验证,最后指出常见的UVM验证开发有哪些坑,以及怎么避免. Introduction 本例使用环境:ModelSim 10.2c,UV ...

  8. Asp.net 程序连接orcle如果在安装 32 位 Oracle 客户端组件的情况下以 64 位模式运行,

    本人使用orcale11g 安装orcale 之类以及navicat配置在这里不提,之后会写一篇文章来说明. 到此已经安装和配置navicat访问数据正常,但是运行Asp.net  程序报错 问题如下 ...

  9. 133A

    #include <stdio.h> #include<string.h> #include <stdbool.h> #define MAXSIZE 105 int ...

  10. 墨刀联合有赞Vant组件库,让你轻松设计出电商原型

    继上周新上线了简历模板之后,本周墨刀的原型模板库又欢喜地增添一名新成员! 有赞Vant组件库 (做电商的宝宝要捂嘴笑了)   Vant 组件库是有赞前端团队开源的一套基于Vue的UI组件库,目前版本收 ...