从零搭建一个Django项目(四):完善项目&通用视图

这里接着上一篇的内容继续从零搭建一个Django项目(三): 视图和模板

现在让我们检视下我们的投票系统已经完成了哪些功能:

  • /polls/ -- 查看问题列表 √
  • /polls/<question_id> -- 查看某一问题选项列表 √
  • /polls/<question_id>/vote -- 投票
  • /polls/<question_id>/result -- 查看投票结果

下面完善下投票功能和结果展示功能

创建一个简单的表单

让我们更新一下在上一个教程中编写的投票详细页面的模板 ("polls/detail.html") ,让它包含一个 HTML form 元素:

<h1>{{ question.question_text }}</h1>

{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}

<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}
{% for choice in question.choice_set.all %}
    <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">
    <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
{% endfor %}
<input type="submit" value="Vote">
</form>
复制代码

简要说明,form元素包裹了一个表单,表单中通过循环渲染了选项的列表,每个选项占一行,每行包括一个单选按钮和一个文字描述,选择后点击提交按钮,触发vote接口调用,调用方式为post并携带参数choice=#(因为radio标签的name属性为choice,#代表对应的value值也就是选项的id)

下面是vote视图的实现

from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse

from .models import Choice, Question
# ...
def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    try:
        selected_choice = question.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        # Redisplay the question voting form.
        return render(request, 'polls/detail.html', {
            'question': question,
            'error_message': "You didn't select a choice.",
        })
    else:
        selected_choice.votes += 1
        selected_choice.save()
        return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))
复制代码

一些补充知识点

  • request.POST 是一个类字典对象,让你可以通过关键字的名字获取提交的数据。 这个例子中, request.POST['choice'] 以字符串形式返回选择的 Choice 的 ID。 request.POST 的值永远是字符串。注意,Django 还以同样的方式提供 request.GET 用于访问 GET 数据 —— 但我们在代码中显式地使用 request.POST ,以保证数据只能通过 POST 调用改动。

  • 如果在 request.POST['choice'] 数据中没有提供 choice , POST 将引发一个 KeyError 。上面的代码检查 KeyError ,如果没有给出 choice 将重新显示 Question 表单和一个错误信息。

  • 在增加 Choice 的得票数之后,代码返回一个 HttpResponseRedirect 而不是常用的 HttpResponse 、 HttpResponseRedirect 只接收一个参数:用户将要被重定向的 URL(请继续看下去,我们将会解释如何构造这个例子中的 URL)。

  • 在这个例子中,我们在 HttpResponseRedirect 的构造函数中使用 reverse() 函数。这个函数避免了我们在视图函数中硬编码 URL。它需要我们给出我们想要跳转的视图的名字和该视图所对应的 URL 模式中需要给该视图提供的参数。 在本例中,使用在 教程第 3 部分 中设定的 URLconf, reverse() 调用将返回一个这样的字符串:

'/polls/3/results/'
复制代码

结果页编写

当有人对 Question 进行投票后, vote() 视图将请求重定向到 Question 的结果界面。让我们来编写这个视图:

from django.shortcuts import get_object_or_404, render

def results(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/results.html', {'question': question})
复制代码

再为其创建一个模板

<h1>{{ question.question_text }}</h1>

<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
{% endfor %}
</ul>

<a href="{% url 'polls:detail' question.id %}">Vote again?</a>
复制代码

至此已经完成了一个简易投票系统所有基础功能的开发
目录结构如下

mysite/
    manage.py
    mysite/
        __init__.py
        settings.py
        urls.py
        wsgi.py
    polls/
        __init__.py
        admin.py
        apps.py
        migrations/
            __init__.py
        templates
            polls/
                detail.html    # 详情页模板
                index.html     # 问题列表页模板
                results.html   # 问题结果页模板
        models.py   # 模型定义
        tests.py
        urls.py     # URL配置
        views.py    # 视图编写
    
复制代码

通用视图

在完成项目基础功能后,回头整体审视下,可以发现大部分视图其实具备相同的流程,Django也为此提供了一次通用视图来减少重复的代码量,改进视图如下:

from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views import generic

from .models import Choice, Question


class IndexView(generic.ListView):
    template_name = 'polls/index.html'
    context_object_name = 'latest_question_list'

    def get_queryset(self):
        """Return the last five published questions."""
        return Question.objects.order_by('-pub_date')[:5]


class DetailView(generic.DetailView):
    model = Question
    template_name = 'polls/detail.html'


class ResultsView(generic.DetailView):
    model = Question
    template_name = 'polls/results.html'


def vote(request, question_id):
    ... # same as above, no changes needed.
复制代码

我们在这里使用两个通用视图: ListView 和 DetailView 。这两个视图分别抽象“显示一个对象列表”和“显示一个特定类型对象的详细信息页面”这两种概念。

  • 每个通用视图需要知道它将作用于哪个模型。 这由 model 属性提供。
  • DetailView 期望从 URL 中捕获名为 "pk" 的主键值,所以我们为通用视图把 question_id 改成 pk 。

url改进如下

from django.urls import path

from . import views

app_name = 'polls'
urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
    path('<int:pk>/', views.DetailView.as_view(), name='detail'),
    path('<int:pk>/results/', views.ResultsView.as_view(), name='results'),
    path('<int:question_id>/vote/', views.vote, name='vote'),
]
复制代码

详细用法,可以参考Django官方文档