Django投票应用选票未统计、多问题页面提交及扩展问题类型的技术咨询
Hey there! Let's tackle your Django poll app issues one by one, starting with the most pressing vote-counting problem, then moving to tests and future extensions.
一、解决多问题页面投票不统计的问题
你的投票没被统计,核心有几个关键问题需要修正:
1. 表单提交目标错误
看你的index.html里,表单的action指向了polls:results,但实际处理投票逻辑的是vote视图!这就导致表单提交后直接跳转到结果页,根本没触发投票计数的代码。
修改index.html里的form标签:
<form action="{% url 'polls:vote' %}" method="post">
别忘了确保你的urls.py里已经配置了polls:vote的路由,比如:
path('vote/', views.vote, name='vote'),
2. 表单name属性与视图取值不匹配(稳定性问题)
你现在用name="{{ caty }}"(也就是Question的__str__返回的question_text)作为表单控件的name,但如果question_text里有空格、特殊字符,会导致request.POST无法正确取值。更稳妥的方式是用问题的id来标识:
修改index.html里的radio输入:
<input type="radio" name="question_{{ caty.id }}" id="choice{{ forloop.counter }}" value="{{ choice.id }}" />
然后在vote视图里,对应地用问题id拼接的key来获取选中的选项id,同时增加异常处理避免崩溃:
@require_http_methods(["POST"]) def vote(request): questions = get_list_or_404(Question) for question in questions: # 拼接对应的表单key form_key = f"question_{question.id}" # 检查用户是否为该问题选择了选项 if form_key not in request.POST: # 可以在这里处理未选择的情况,比如跳过或者返回错误提示 continue try: selected_choice = question.choice_set.get(pk=request.POST[form_key]) # 使用F()表达式避免并发更新的竞态条件(多用户同时投票时计数错误) selected_choice.votes = F('votes') + 1 selected_choice.save() except Choice.DoesNotExist: # 处理选项不存在的异常,返回带错误提示的页面 return render(request, 'polls/index.html', { 'question_list': questions, 'error_message': f"Invalid choice for question: {question.question_text}", }) return HttpResponseRedirect(reverse("polls:results"))
3. 竞态条件修复
原来的selected_choice.votes +=1在多用户同时投票时可能导致计数错误,用Django的F()表达式可以直接在数据库层面更新,避免这个问题,上面的代码已经修正了这一点。
二、编写多问题投票的单元测试
要模拟多问题的表单提交,你需要先创建测试用的Question和Choice对象,再构造包含多个问题选项的POST数据:
修改你的tests.py里的测试方法:
def test_multiple_question_vote(self): # 创建测试用的两个问题和对应选项 q1 = Question.objects.create(question_text="Favorite color?", pub_date=timezone.now()) q1_choice1 = Choice.objects.create(question=q1, choice_text="Red", votes=0) q1_choice2 = Choice.objects.create(question=q1, choice_text="Blue", votes=0) q2 = Question.objects.create(question_text="Favorite food?", pub_date=timezone.now()) q2_choice1 = Choice.objects.create(question=q2, choice_text="Pizza", votes=0) q2_choice2 = Choice.objects.create(question=q2, choice_text="Burger", votes=0) client = Client() # 构造POST数据,对应每个问题的选中选项 post_data = { f"question_{q1.id}": q1_choice1.id, f"question_{q2.id}": q2_choice2.id, } # 提交到vote视图,follow=True跟踪重定向 response = client.post(reverse('polls:vote'), post_data, follow=True) # 检查是否跳转到结果页 self.assertRedirects(response, reverse('polls:results')) # 重新从数据库获取选项,检查投票数是否增加 q1_choice1.refresh_from_db() q2_choice2.refresh_from_db() self.assertEqual(q1_choice1.votes, 1) self.assertEqual(q2_choice2.votes, 1)
三、扩展问题类型(自由文本等)的方案选择
关于是否新建app还是修改现有models,取决于你的需求复杂度:
方案1:修改现有models(适合轻量需求)
如果自由文本问题和选择题属于同一类问卷内容,不需要完全独立管理,可以:
- 给
Question模型新增一个类型字段,标记是选择题还是自由文本:
class Question(models.Model): QUESTION_TYPES = [ ('CHOICE', 'Choice'), ('TEXT', 'Free Text'), ] question_text = models.CharField(max_length=200) pub_date = models.DateTimeField("date published") question_type = models.CharField(max_length=10, choices=QUESTION_TYPES, default='CHOICE') # 原有方法保持不变...
- 新增一个
TextResponse模型,存储用户的自由文本回答:
class TextResponse(models.Model): question = models.ForeignKey(Question, on_delete=models.CASCADE) response_text = models.TextField() submitted_at = models.DateTimeField(auto_now_add=True)
然后在模板里根据question_type渲染不同的表单控件(radio组或者textarea),在vote视图里分别处理选择题计数和文本回答的存储。
方案2:新建独立app(适合复杂需求)
如果自由文本问卷和选择题问卷是完全独立的业务,或者未来会有更多不同类型的问题,新建一个surveys或者text_pollsapp会更清晰,避免把不同业务逻辑混在一个app里。不过如果你的表单比较静态,这个方案可能有点过重,方案1更适合你。
另外,你提到有人警告不要在html里硬编码问题/选项,这是对的,但你现在通过Django模板动态渲染控件的方式是没问题的——只需要维护数据库里的问题和选项,不需要修改模板代码,完全符合最佳实践。
备注:内容来源于stack exchange,提问作者pizzapizzapizza

