You need to enable JavaScript to run this app. Django投票应用如何调整现有模型以支持文本应答与多选应答功能
优惠活动
大模型
产品
解决方案
定价
更多

Django投票应用如何调整现有模型以支持文本应答与多选应答功能

核心问题定位

你现在的实现之所以不好扩展,根本原因是把投票选项、选项计数硬编码在Poll主表上,最多固定3个选项,没有预留多选、自由文本的配置位,要实现目标功能必须先把模型结构拆开,再配套调整视图和模板逻辑。

1. 模型层重构(models.py调整)

把原来强耦合的字段全部拆解,建立三层关联结构:

  • 保留Poll主表,只存投票本身的基础信息、规则配置
  • 新建Option表,和Poll做一对多关联,支持每个投票挂任意数量的选项,每个选项独立存得票数、标记是否为自由文本输入项
  • 新建VoteResponse表,存每个用户提交的应答内容,支持关联多个选项、保存自由文本输入

调整后的模型代码:

from django.db import models
from django.utils import timezone

class Poll(models.Model):
    title = models.CharField(max_length=200)
    question = models.TextField()
    # 新增投票规则配置
    allow_multi_select = models.BooleanField(default=False, verbose_name="是否允许多选")
    allow_free_text = models.BooleanField(default=False, verbose_name="是否允许自由文本回复")
    active_from = models.DateTimeField(default=timezone.now)
    active_for = models.IntegerField(default=0, help_text="投票有效时长(小时),0为永久有效")

    @property
    def is_active(self):
        # 补全原来代码缺失的is_active逻辑,避免调用时报错
        if self.active_for == 0:
            return True
        expire_time = self.active_from + timezone.timedelta(hours=self.active_for)
        return timezone.now() < expire_time

class Option(models.Model):
    poll = models.ForeignKey(Poll, on_delete=models.CASCADE, related_name="options")
    content = models.CharField(max_length=100, verbose_name="选项内容")
    vote_count = models.IntegerField(default=0, verbose_name="该选项得票数")
    is_text_input = models.BooleanField(default=False, verbose_name="该选项关联自由文本输入框")

class VoteResponse(models.Model):
    poll = models.ForeignKey(Poll, on_delete=models.CASCADE, related_name="responses")
    selected_options = models.ManyToManyField(Option, blank=True, verbose_name="用户选中的固定选项")
    free_text_content = models.TextField(blank=True, null=True, verbose_name="用户提交的自由文本内容")
    submit_time = models.DateTimeField(auto_now_add=True)

2. 投票视图逻辑调整(views.py调整)

把原来硬编码匹配option1/2/3的分支逻辑全部替换,适配单选、多选、自由文本三种场景:

  • 替换掉直接抛异常的Poll.objects.get(),改用get_object_or_404优化错误体验
  • 用request.POST.getlist()统一获取用户选中的选项,兼容单选和多选的传参格式
  • 前置校验:过滤掉不属于当前投票的非法选项值,根据投票规则检查是否越权多选
  • 先把用户的应答存入VoteResponse表,再遍历每个选中的选项累加票数,最后把自由文本内容存入应答记录

调整后的核心vote视图代码:

from django.shortcuts import get_object_or_404, redirect, render
from django.http import HttpResponseBadRequest
from django.contrib import messages
from .models import Poll, Option, VoteResponse

def vote(request, poll_id):
    poll = get_object_or_404(Poll, pk=poll_id)
    # 投票过期直接提示并跳回首页
    if not poll.is_active:
        messages.info(request, 'The poll is closed')
        return redirect('home')
    
    if request.method == 'POST':
        # 获取当前投票下的所有合法选项id,做请求校验
        valid_option_ids = set(poll.options.values_list('id', flat=True))
        # 统一获取所有选中的选项,单/多选兼容
        selected_option_ids = request.POST.getlist('poll_options', [])
        # 过滤掉不属于当前投票的非法请求值
        valid_selected_ids = [int(oid) for oid in selected_option_ids if int(oid) in valid_option_ids]

        # 规则校验
        if not poll.allow_multi_select and len(valid_selected_ids) > 1:
            return HttpResponseBadRequest("当前投票不允许多选")
        if len(valid_selected_ids) == 0 and not poll.allow_free_text:
            return HttpResponseBadRequest("请至少选择一个选项")
        
        # 创建用户的应答记录
        response = VoteResponse.objects.create(poll=poll)
        # 处理选中的固定选项
        if valid_selected_ids:
            selected_opts = Option.objects.filter(id__in=valid_selected_ids)
            response.selected_options.set(selected_opts)
            # 给每个选项累计票数
            for opt in selected_opts:
                opt.vote_count += 1
                opt.save()
        
        # 处理自由文本内容
        if poll.allow_free_text:
            text_input = request.POST.get('free_text_input', '').strip()
            if text_input:
                response.free_text_content = text_input
                response.save()

        return redirect('results', poll.id)
    
    context = {
        'poll': poll,
        'options': poll.options.all()
    }
    return render(request, 'poll/vote.html', context)

3. 模板层适配

把原来写死三个单选按钮的投票页、结果页代码替换成动态渲染:

投票页(vote.html)核心逻辑

<form method="POST">
    {% csrf_token %}
    <h3>{{ poll.title }}</h3>
    <p>{{ poll.question }}</p>
    <!-- 根据投票配置自动切换单选/复选框 -->
    {% for option in options %}
        {% if poll.allow_multi_select %}
            <input type="checkbox" name="poll_options" value="{{ option.id }}">
        {% else %}
            <input type="radio" name="poll_options" value="{{ option.id }}" required>
        {% endif %}
        <label>{{ option.content }}</label>
        <br>
    {% endfor %}

    <!-- 允许自由文本时动态显示输入框 -->
    {% if poll.allow_free_text %}
        <div style="margin-top:1rem">
            <label>自由回复:</label>
            <textarea name="free_text_input" rows="3" placeholder="请输入你的想法"></textarea>
        </div>
    {% endif %}

    <button type="submit" style="margin-top:1rem">提交投票</button>
</form>

结果页(results.html)核心逻辑

<h3>{{ poll.title }} 投票结果</h3>
<!-- 动态遍历所有选项显示得票 -->
{% for option in poll.options.all %}
    <p>{{ option.content }}:{{ option.vote_count }} 票</p>
{% endfor %}

<!-- 有自由文本回复时动态展示所有用户提交的内容 -->
{% if poll.allow_free_text %}
    <h4>用户自由回复汇总</h4>
    <ul>
        {% for resp in poll.responses.all %}
            {% if resp.free_text_content %}
                <li>{{ resp.free_text_content }}</li>
            {% endif %}
        {% endfor %}
    </ul>
{% endif %}

新手注意事项

  • 模型结构调整后,记得执行makemigrations和migrate生成数据库表,同时手动把原来硬编码选项对应的历史投票数据迁移到新的Option表,避免旧数据丢失
  • 处理多选项投票时,一定要校验选项和当前投票的归属关系,不能直接信任前端传的参数,防止恶意请求刷其他投票的票数
  • 自由文本内容是用户提交的应答,不要存在Poll主表上,必须存在独立的VoteResponse表,不然多用户提交的内容会互相覆盖

内容的提问来源于stack exchange,提问作者Mr Dot

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.11.01 15:06:05