一、编写表单实现投票功能1.1 更新详情页模板首先我们需要在问题详情页添加一个投票表单。更新polls/templates/polls/detail.htmlh1{{ question.question_text }}/h1{% if error_message %}pstylecolor:red;strong{{ error_message }}/strong/p{% endif %}formaction{% url polls:vote question.id %}methodpost{% csrf_token %} {% for choice in question.choice_set.all %}inputtyperadionamechoiceidchoice{{ forloop.counter }}value{{ choice.id }}labelforchoice{{ forloop.counter }}{{ choice.choice_text }}/labelbr{% endfor %}inputtypesubmitvalueVote/form关键知识点元素说明{% csrf_token %}Django 的 CSRF 防护令牌POST 表单必须包含forloop.counter模板提供的循环计数器从 1 开始namechoice表单字段名提交后可通过request.POST[choice]获取value{{ choice.id }}单选按钮的值即选项的 ID1.2 实现 vote 视图现在我们来实现真正处理投票逻辑的vote视图。更新polls/views.pyfromdjango.httpimportHttpResponseRedirectfromdjango.shortcutsimportget_object_or_404,renderfromdjango.urlsimportreversefrom.modelsimportChoice,Questiondefvote(request,question_id):questionget_object_or_404(Question,pkquestion_id)try:# 从 POST 数据中获取选择的选项 IDselected_choicequestion.choice_set.get(pkrequest.POST[choice])except(KeyError,Choice.DoesNotExist):# 未选择选项时重新显示表单并提示错误returnrender(request,polls/detail.html,{question:question,error_message:You didnt select a choice.,})else:# 增加投票数并保存selected_choice.votes1selected_choice.save()# 重定向到结果页面防止重复提交returnHttpResponseRedirect(reverse(polls:results,args(question.id,)))代码解析request.POST类似字典的对象包含所有 POST 提交的表单数据request.POST[choice]返回所选选项 ID 的字符串形式HttpResponseRedirect处理成功后返回重定向响应这是 Web 开发的最佳实践——防止用户点击后退按钮导致数据重复提交reverse()通过 URL 名称反向解析 URL避免硬编码1.3 实现 results 视图和模板投票后需要展示结果创建results视图defresults(request,question_id):questionget_object_or_404(Question,pkquestion_id)returnrender(request,polls/results.html,{question:question})创建polls/templates/polls/results.htmlh1{{ question.question_text }}/h1ul{% for choice in question.choice_set.all %}li{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}/li{% endfor %}/ulahref{% url polls:detail question.id %}Vote again?/a模板过滤器pluralize自动处理复数形式当votes不为 1 时显示 “votes”为 1 时显示 “vote”二、通用视图代码越少越好2.1 为什么要用通用视图观察我们的index、detail、results三个视图它们都遵循相同的模式从 URL 参数获取数据查询数据库加载模板返回渲染后的响应这种根据 URL 参数从数据库获取数据并渲染模板的模式在 Web 开发中极其常见。Django 提供了通用视图系统来抽象这些常见模式让我们无需编写重复的 Python 代码 。2.2 转换步骤一改良 URLconf修改polls/urls.py使用.as_view()方法将类视图转换为函数视图fromdjango.urlsimportpathfrom.importviews app_namepollsurlpatterns[path(,views.IndexView.as_view(),nameindex),path(int:pk/,views.DetailView.as_view(),namedetail),path(int:pk/results/,views.ResultsView.as_view(),nameresults),path(int:question_id/vote/,views.vote,namevote),]重要变化将question_id改为pk因为DetailView期望从 URL 中捕获名为pk的主键值2.3 转换步骤二改良视图删除旧的函数视图使用 Django 的通用视图重写polls/views.pyfromdjango.httpimportHttpResponseRedirectfromdjango.shortcutsimportget_object_or_404,renderfromdjango.urlsimportreversefromdjango.viewsimportgenericfrom.modelsimportChoice,QuestionclassIndexView(generic.ListView):template_namepolls/index.htmlcontext_object_namelatest_question_listdefget_queryset(self):返回最近发布的5个问题returnQuestion.objects.order_by(-pub_date)[:5]classDetailView(generic.DetailView):modelQuestion template_namepolls/detail.htmlclassResultsView(generic.DetailView):modelQuestion template_namepolls/results.htmldefvote(request,question_id):# 保持不变通用视图不适用于处理表单提交questionget_object_or_404(Question,pkquestion_id)try:selected_choicequestion.choice_set.get(pkrequest.POST[choice])except(KeyError,Choice.DoesNotExist):returnrender(request,polls/detail.html,{question:question,error_message:You didnt select a choice.,})else:selected_choice.votes1selected_choice.save()returnHttpResponseRedirect(reverse(polls:results,args(question.id,)))2.4 通用视图详解ListView列表视图属性说明template_name指定模板路径默认polls/question_list.htmlcontext_object_name自定义上下文变量名默认question_listget_queryset()定义要展示的对象列表DetailView详情视图属性说明model指定数据模型template_name指定模板路径默认polls/question_detail.htmlpk从 URL 捕获的主键值自动上下文变量对于DetailViewDjango 会自动将模型对象添加到上下文中变量名默认为模型名的小写形式即question三、完整代码汇总polls/views.py最终版fromdjango.httpimportHttpResponseRedirectfromdjango.shortcutsimportget_object_or_404,renderfromdjango.urlsimportreversefromdjango.viewsimportgenericfrom.modelsimportChoice,Question# 通用视图问题列表classIndexView(generic.ListView):template_namepolls/index.htmlcontext_object_namelatest_question_listdefget_queryset(self):returnQuestion.objects.order_by(-pub_date)[:5]# 通用视图问题详情classDetailView(generic.DetailView):modelQuestion template_namepolls/detail.html# 通用视图投票结果classResultsView(generic.DetailView):modelQuestion template_namepolls/results.html# 函数视图处理投票通用视图不适用defvote(request,question_id):questionget_object_or_404(Question,pkquestion_id)try:selected_choicequestion.choice_set.get(pkrequest.POST[choice])except(KeyError,Choice.DoesNotExist):returnrender(request,polls/detail.html,{question:question,error_message:You didnt select a choice.,})else:selected_choice.votes1selected_choice.save()returnHttpResponseRedirect(reverse(polls:results,args(question.id,)))polls/urls.py最终版fromdjango.urlsimportpathfrom.importviews app_namepollsurlpatterns[path(,views.IndexView.as_view(),nameindex),path(int:pk/,views.DetailView.as_view(),namedetail),path(int:pk/results/,views.ResultsView.as_view(),nameresults),path(int:question_id/vote/,views.vote,namevote),]模板文件结构polls/ templates/ polls/ index.html # 问题列表 detail.html # 投票表单 results.html # 投票结果四、关键概念总结4.1 表单处理流程用户访问 detail 页面 → 选择选项并提交 → POST 请求到 vote 视图 ↓ 结果页面 ← 重定向 ← 保存数据 ← 验证成功 ← 获取选项对象 ← 解析 choice ID ↑ └──── 验证失败 ────→ 返回 detail 页面并显示错误4.2 通用视图 vs 函数视图场景推荐方案展示对象列表ListView展示单个对象详情DetailView处理表单提交函数视图或FormView复杂业务逻辑函数视图或类视图4.3 安全注意事项CSRF 防护所有修改数据的 POST 表单都必须包含{% csrf_token %}竞态条件当前vote视图存在并发问题。如果两个用户同时投票可能会丢失票数。生产环境应使用 F() 表达式fromdjango.db.modelsimportF# 更安全的投票方式selected_choice.votesF(votes)1selected_choice.save()五、下一步现在您可以启动开发服务器python manage.py runserver访问http://127.0.0.1:8000/polls/查看效果尝试投票并观察结果更新参考资源Django 6.0 官方文档 - 编写你的第一个 Django 应用第 4 部分Django 通用视图文档
Django 学习 Part 4: 表单处理与通用视图
一、编写表单实现投票功能1.1 更新详情页模板首先我们需要在问题详情页添加一个投票表单。更新polls/templates/polls/detail.htmlh1{{ question.question_text }}/h1{% if error_message %}pstylecolor:red;strong{{ error_message }}/strong/p{% endif %}formaction{% url polls:vote question.id %}methodpost{% csrf_token %} {% for choice in question.choice_set.all %}inputtyperadionamechoiceidchoice{{ forloop.counter }}value{{ choice.id }}labelforchoice{{ forloop.counter }}{{ choice.choice_text }}/labelbr{% endfor %}inputtypesubmitvalueVote/form关键知识点元素说明{% csrf_token %}Django 的 CSRF 防护令牌POST 表单必须包含forloop.counter模板提供的循环计数器从 1 开始namechoice表单字段名提交后可通过request.POST[choice]获取value{{ choice.id }}单选按钮的值即选项的 ID1.2 实现 vote 视图现在我们来实现真正处理投票逻辑的vote视图。更新polls/views.pyfromdjango.httpimportHttpResponseRedirectfromdjango.shortcutsimportget_object_or_404,renderfromdjango.urlsimportreversefrom.modelsimportChoice,Questiondefvote(request,question_id):questionget_object_or_404(Question,pkquestion_id)try:# 从 POST 数据中获取选择的选项 IDselected_choicequestion.choice_set.get(pkrequest.POST[choice])except(KeyError,Choice.DoesNotExist):# 未选择选项时重新显示表单并提示错误returnrender(request,polls/detail.html,{question:question,error_message:You didnt select a choice.,})else:# 增加投票数并保存selected_choice.votes1selected_choice.save()# 重定向到结果页面防止重复提交returnHttpResponseRedirect(reverse(polls:results,args(question.id,)))代码解析request.POST类似字典的对象包含所有 POST 提交的表单数据request.POST[choice]返回所选选项 ID 的字符串形式HttpResponseRedirect处理成功后返回重定向响应这是 Web 开发的最佳实践——防止用户点击后退按钮导致数据重复提交reverse()通过 URL 名称反向解析 URL避免硬编码1.3 实现 results 视图和模板投票后需要展示结果创建results视图defresults(request,question_id):questionget_object_or_404(Question,pkquestion_id)returnrender(request,polls/results.html,{question:question})创建polls/templates/polls/results.htmlh1{{ question.question_text }}/h1ul{% for choice in question.choice_set.all %}li{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}/li{% endfor %}/ulahref{% url polls:detail question.id %}Vote again?/a模板过滤器pluralize自动处理复数形式当votes不为 1 时显示 “votes”为 1 时显示 “vote”二、通用视图代码越少越好2.1 为什么要用通用视图观察我们的index、detail、results三个视图它们都遵循相同的模式从 URL 参数获取数据查询数据库加载模板返回渲染后的响应这种根据 URL 参数从数据库获取数据并渲染模板的模式在 Web 开发中极其常见。Django 提供了通用视图系统来抽象这些常见模式让我们无需编写重复的 Python 代码 。2.2 转换步骤一改良 URLconf修改polls/urls.py使用.as_view()方法将类视图转换为函数视图fromdjango.urlsimportpathfrom.importviews app_namepollsurlpatterns[path(,views.IndexView.as_view(),nameindex),path(int:pk/,views.DetailView.as_view(),namedetail),path(int:pk/results/,views.ResultsView.as_view(),nameresults),path(int:question_id/vote/,views.vote,namevote),]重要变化将question_id改为pk因为DetailView期望从 URL 中捕获名为pk的主键值2.3 转换步骤二改良视图删除旧的函数视图使用 Django 的通用视图重写polls/views.pyfromdjango.httpimportHttpResponseRedirectfromdjango.shortcutsimportget_object_or_404,renderfromdjango.urlsimportreversefromdjango.viewsimportgenericfrom.modelsimportChoice,QuestionclassIndexView(generic.ListView):template_namepolls/index.htmlcontext_object_namelatest_question_listdefget_queryset(self):返回最近发布的5个问题returnQuestion.objects.order_by(-pub_date)[:5]classDetailView(generic.DetailView):modelQuestion template_namepolls/detail.htmlclassResultsView(generic.DetailView):modelQuestion template_namepolls/results.htmldefvote(request,question_id):# 保持不变通用视图不适用于处理表单提交questionget_object_or_404(Question,pkquestion_id)try:selected_choicequestion.choice_set.get(pkrequest.POST[choice])except(KeyError,Choice.DoesNotExist):returnrender(request,polls/detail.html,{question:question,error_message:You didnt select a choice.,})else:selected_choice.votes1selected_choice.save()returnHttpResponseRedirect(reverse(polls:results,args(question.id,)))2.4 通用视图详解ListView列表视图属性说明template_name指定模板路径默认polls/question_list.htmlcontext_object_name自定义上下文变量名默认question_listget_queryset()定义要展示的对象列表DetailView详情视图属性说明model指定数据模型template_name指定模板路径默认polls/question_detail.htmlpk从 URL 捕获的主键值自动上下文变量对于DetailViewDjango 会自动将模型对象添加到上下文中变量名默认为模型名的小写形式即question三、完整代码汇总polls/views.py最终版fromdjango.httpimportHttpResponseRedirectfromdjango.shortcutsimportget_object_or_404,renderfromdjango.urlsimportreversefromdjango.viewsimportgenericfrom.modelsimportChoice,Question# 通用视图问题列表classIndexView(generic.ListView):template_namepolls/index.htmlcontext_object_namelatest_question_listdefget_queryset(self):returnQuestion.objects.order_by(-pub_date)[:5]# 通用视图问题详情classDetailView(generic.DetailView):modelQuestion template_namepolls/detail.html# 通用视图投票结果classResultsView(generic.DetailView):modelQuestion template_namepolls/results.html# 函数视图处理投票通用视图不适用defvote(request,question_id):questionget_object_or_404(Question,pkquestion_id)try:selected_choicequestion.choice_set.get(pkrequest.POST[choice])except(KeyError,Choice.DoesNotExist):returnrender(request,polls/detail.html,{question:question,error_message:You didnt select a choice.,})else:selected_choice.votes1selected_choice.save()returnHttpResponseRedirect(reverse(polls:results,args(question.id,)))polls/urls.py最终版fromdjango.urlsimportpathfrom.importviews app_namepollsurlpatterns[path(,views.IndexView.as_view(),nameindex),path(int:pk/,views.DetailView.as_view(),namedetail),path(int:pk/results/,views.ResultsView.as_view(),nameresults),path(int:question_id/vote/,views.vote,namevote),]模板文件结构polls/ templates/ polls/ index.html # 问题列表 detail.html # 投票表单 results.html # 投票结果四、关键概念总结4.1 表单处理流程用户访问 detail 页面 → 选择选项并提交 → POST 请求到 vote 视图 ↓ 结果页面 ← 重定向 ← 保存数据 ← 验证成功 ← 获取选项对象 ← 解析 choice ID ↑ └──── 验证失败 ────→ 返回 detail 页面并显示错误4.2 通用视图 vs 函数视图场景推荐方案展示对象列表ListView展示单个对象详情DetailView处理表单提交函数视图或FormView复杂业务逻辑函数视图或类视图4.3 安全注意事项CSRF 防护所有修改数据的 POST 表单都必须包含{% csrf_token %}竞态条件当前vote视图存在并发问题。如果两个用户同时投票可能会丢失票数。生产环境应使用 F() 表达式fromdjango.db.modelsimportF# 更安全的投票方式selected_choice.votesF(votes)1selected_choice.save()五、下一步现在您可以启动开发服务器python manage.py runserver访问http://127.0.0.1:8000/polls/查看效果尝试投票并观察结果更新参考资源Django 6.0 官方文档 - 编写你的第一个 Django 应用第 4 部分Django 通用视图文档