Django入门与实践-第19章:主题回复

现在让我们来实现回复帖子的功能,以便我们可以添加更多的数据和改进功能实现与单元测试。

5-9.png

添加新的URL路由:

myproject/urls.py(完整代码)

  1. url(r'^boards/(?P<pk>\d+)/topics/(?P<topic_pk>\d+)/reply/$', views.reply_topic, name='reply_topic'),

给回帖创建一个新的表单:

boards/forms.py (完整代码)

  1. from django import forms
  2. from .models import Post
  3. class PostForm(forms.ModelForm):
  4. class Meta:
  5. model = Post
  6. fields = ['message', ]

一个新的受@login_required保护的视图,以及简单的表单处理逻辑

boards/views.py(完整代码)

  1. from django.contrib.auth.decorators import login_required
  2. from django.shortcuts import get_object_or_404, redirect, render
  3. from .forms import PostForm
  4. from .models import Topic
  5. @login_required
  6. def reply_topic(request, pk, topic_pk):
  7. topic = get_object_or_404(Topic, board__pk=pk, pk=topic_pk)
  8. if request.method == 'POST':
  9. form = PostForm(request.POST)
  10. if form.is_valid():
  11. post = form.save(commit=False)
  12. post.topic = topic
  13. post.created_by = request.user
  14. post.save()
  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})

现在我们再会到new_topic视图函数,更新重定向地址(标记为 #TODO 的地方)

  1. @login_required
  2. def new_topic(request, pk):
  3. board = get_object_or_404(Board, pk=pk)
  4. if request.method == 'POST':
  5. form = NewTopicForm(request.POST)
  6. if form.is_valid():
  7. topic = form.save(commit=False)
  8. # code suppressed ...
  9. return redirect('topic_posts', pk=pk, topic_pk=topic.pk) # <- here
  10. # code suppressed ...

值得注意的是:在视图函数replay_topic中,我们使用topic_pk,因为我们引用的是函数的关键字参数,而在new_topic视图中,我们使用的是topic.pk,因为topic是一个对象(Topic模型的实例对象),.pk是这个实例对象的一个属性,这两种细微的差别,其实区别很大,别搞混了。

回复页面模版的一个版本:

templates/reply_topic.html

  1. {% extends 'base.html' %}
  2. {% load static %}
  3. {% block title %}Post a reply{% endblock %}
  4. {% block breadcrumb %}
  5. <li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li>
  6. <li class="breadcrumb-item"><a href="{% url 'board_topics' topic.board.pk %}">{{ topic.board.name }}</a></li>
  7. <li class="breadcrumb-item"><a href="{% url 'topic_posts' topic.board.pk topic.pk %}">{{ topic.subject }}</a></li>
  8. <li class="breadcrumb-item active">Post a reply</li>
  9. {% endblock %}
  10. {% block content %}
  11. <form method="post" class="mb-4">
  12. {% csrf_token %}
  13. {% include 'includes/form.html' %}
  14. <button type="submit" class="btn btn-success">Post a reply</button>
  15. </form>
  16. {% for post in topic.posts.all %}
  17. <div class="card mb-2">
  18. <div class="card-body p-3">
  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. </div>
  29. </div>
  30. {% endfor %}
  31. {% endblock %}

5-10.png

提交回复之后,用户会跳回主题的回复列表:

5-11.png

我们可以改变第一条帖子的样式,使得它在页面上更突出:

templates/topic_posts.html(完整代码)

  1. {% for post in topic.posts.all %}
  2. <div class="card mb-2 {% if forloop.first %}border-dark{% endif %}">
  3. {% if forloop.first %}
  4. <div class="card-header text-white bg-dark py-2 px-3">{{ topic.subject }}</div>
  5. {% endif %}
  6. <div class="card-body p-3">
  7. <!-- code suppressed -->
  8. </div>
  9. </div>
  10. {% endfor %}

5-12.png

现在对于测试,已经实现标准化流程了,就像我们迄今为止所做的一样。 在boards/tests 目录中创建一个新文件 test_view_reply_topic.py

boards/tests/test_view_reply_topic.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 reply_topic
  6. class ReplyTopicTestCase(TestCase):
  7. '''
  8. Base test case to be used in all `reply_topic` view tests
  9. '''
  10. def setUp(self):
  11. self.board = Board.objects.create(name='Django', description='Django board.')
  12. self.username = 'john'
  13. self.password = '123'
  14. user = User.objects.create_user(username=self.username, email='john@doe.com', password=self.password)
  15. self.topic = Topic.objects.create(subject='Hello, world', board=self.board, starter=user)
  16. Post.objects.create(message='Lorem ipsum dolor sit amet', topic=self.topic, created_by=user)
  17. self.url = reverse('reply_topic', kwargs={'pk': self.board.pk, 'topic_pk': self.topic.pk})
  18. class LoginRequiredReplyTopicTests(ReplyTopicTestCase):
  19. # ...
  20. class ReplyTopicTests(ReplyTopicTestCase):
  21. # ...
  22. class SuccessfulReplyTopicTests(ReplyTopicTestCase):
  23. # ...
  24. class InvalidReplyTopicTests(ReplyTopicTestCase):
  25. # ...

这里的精髓在于自定义了测试用例基类ReplyTopicTestCase。然后所有四个类将继承这个测试用例。

首先,我们测试视图是否受@login_required装饰器保护,然后检查HTML输入,状态码。最后,我们测试一个有效和无效的表单提交。