You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多题型测验应用设计方案及Django模型动态字段实现咨询

测验应用题型动态字段实现方案

我正在开发一款支持多种题型的测验应用,包含选择题(MCQ)、简答题(SAQ)、音视频题(IVQ)三类题型:

  • 选择题需支持添加选项并标记正确选项
  • 简答题包含题干和答案字段
  • 音视频题的题干为音视频文件,同时包含作答字段

目前已基于Django抽象基类Question实现了三类题型的模型,但需要实现贡献者添加题目时,选择题型后自动显示对应字段的功能(比如选MCQ显示选项添加及正确选项标记字段,选SAQ显示答案字段)。

现有模型代码如下:

class Question(models.Model):

    QUESTION_TYPE_CHOICES = [
        ('MCQ', 'MultipleChoiceQuestion'),
        ('SAQ', 'ShortAnswerQuestion'),
        ('IVQ', 'ImageVideoQuestion'),
    ]
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    paper = models.ForeignKey(Paper, on_delete=models.CASCADE, null=True)
    question_type = models.CharField(max_length=200,choices=QUESTION_TYPE_CHOICES, null=True)
    question_text = models.CharField(max_length=200)

    def __str__(self):
        return self.question_text
    class Meta:
        abstract = True


class MultipleChoiceQuestion(Question):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    paper = models.ForeignKey(Paper, on_delete=models.CASCADE, null=True)
    multiple_choice_option_text = models.CharField(max_length=200, default=None)

    def __str__(self):
        return self.question_text

class MultipleChoiceOption(models.Model):
    multiple_choice_question = models.ForeignKey(MultipleChoiceQuestion, on_delete=models.CASCADE, null=True)
    multiple_choice_option_text = models.CharField(max_length=200, default=None)
    is_correct = models.BooleanField(default=False)

    def __str__(self):
        return self.multiple_choice_option_text

class ShortAnswerQuestion(Question):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    paper = models.ForeignKey(Paper, on_delete=models.CASCADE, null=True)
    answer_text = models.TextField()
    discussion = models.TextField(blank=True)

    def __str__(self):
        return self.question_text

class ImageVideoQuestion(Question):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    paper = models.ForeignKey(Paper, on_delete=models.CASCADE, null=True)
    answer_text = models.TextField()
    discussion = models.TextField(blank=True)

    def __str__(self):
        return self.question_text

一、模型优化

现有模型存在冗余问题:子类无需重复定义id和paper字段(抽象基类已继承);MultipleChoiceQuestion中的multiple_choice_option_text字段多余,已通过MultipleChoiceOption关联管理选项。优化后的模型如下:

import uuid
from django.db import models

class Paper(models.Model):
    title = models.CharField(max_length=200)
    # 其他字段根据业务需求补充

class Question(models.Model):
    QUESTION_TYPE_CHOICES = [
        ('MCQ', '选择题'),
        ('SAQ', '简答题'),
        ('IVQ', '音视频题'),
    ]
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    paper = models.ForeignKey(Paper, on_delete=models.CASCADE, null=True, related_name='questions')
    question_type = models.CharField(max_length=20, choices=QUESTION_TYPE_CHOICES)
    question_text = models.CharField(max_length=200)

    def __str__(self):
        return self.question_text

    class Meta:
        abstract = True

class MultipleChoiceQuestion(Question):
    def __str__(self):
        return f"选择题: {self.question_text}"

class MultipleChoiceOption(models.Model):
    question = models.ForeignKey(MultipleChoiceQuestion, on_delete=models.CASCADE, related_name='options')
    option_text = models.CharField(max_length=200)
    is_correct = models.BooleanField(default=False)

    def __str__(self):
        return self.option_text

class ShortAnswerQuestion(Question):
    answer_text = models.TextField()
    discussion = models.TextField(blank=True)

    def __str__(self):
        return f"简答题: {self.question_text}"

class ImageVideoQuestion(Question):
    media_file = models.FileField(upload_to='question_media/', null=True, help_text="上传音视频文件作为题干")
    answer_text = models.TextField()
    discussion = models.TextField(blank=True)

    def __str__(self):
        return f"音视频题: {self.question_text}"

二、动态字段实现方案

要实现选择题型后自动显示对应字段,需要结合Django表单和前端JavaScript完成:

1. 后端表单处理

方案1:单一表单+动态字段+FormSet

定义基础表单,根据题型动态添加对应字段,用FormSet处理选择题的选项:

from django import forms
from django.forms import inlineformset_factory
from .models import (
    MultipleChoiceQuestion, MultipleChoiceOption,
    ShortAnswerQuestion, ImageVideoQuestion
)

class BaseQuestionForm(forms.Form):
    question_type = forms.ChoiceField(choices=Question.QUESTION_TYPE_CHOICES)
    question_text = forms.CharField(max_length=200)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        question_type = self.data.get('question_type') or self.initial.get('question_type')
        if question_type:
            self.add_specific_fields(question_type)

    def add_specific_fields(self, question_type):
        if question_type in ['SAQ', 'IVQ']:
            self.fields['answer_text'] = forms.CharField(widget=forms.Textarea)
            self.fields['discussion'] = forms.CharField(widget=forms.Textarea, required=False)
            if question_type == 'IVQ':
                self.fields['media_file'] = forms.FileField(required=False)

# 选择题选项的FormSet,默认显示4个选项输入框
MCQOptionFormSet = inlineformset_factory(
    MultipleChoiceQuestion, MultipleChoiceOption,
    fields=('option_text', 'is_correct'),
    extra=4,
    can_delete=True
)

方案2:多模型表单切换

为每种题型单独定义模型表单,在视图中根据提交的题型选择对应表单处理:

class MCQForm(forms.ModelForm):
    class Meta:
        model = MultipleChoiceQuestion
        fields = ['question_text', 'paper']

class SAQForm(forms.ModelForm):
    class Meta:
        model = ShortAnswerQuestion
        fields = ['question_text', 'answer_text', 'discussion', 'paper']

class IVQForm(forms.ModelForm):
    class Meta:
        model = ImageVideoQuestion
        fields = ['question_text', 'media_file', 'answer_text', 'discussion', 'paper']

视图逻辑示例:

from django.shortcuts import render, redirect
from .forms import MCQForm, SAQForm, IVQForm, MCQOptionFormSet

def add_question(request):
    if request.method == 'POST':
        question_type = request.POST.get('question_type')
        if question_type == 'MCQ':
            form = MCQForm(request.POST)
            formset = MCQOptionFormSet(request.POST)
            if form.is_valid() and formset.is_valid():
                question = form.save()
                formset.instance = question
                formset.save()
                return redirect('question_list')
        elif question_type == 'SAQ':
            form = SAQForm(request.POST)
            if form.is_valid():
                form.save()
                return redirect('question_list')
        elif question_type == 'IVQ':
            form = IVQForm(request.POST, request.FILES)
            if form.is_valid():
                form.save()
                return redirect('question_list')
    return render(request, 'add_question.html')

2. 前端动态显示逻辑

在模板中通过JavaScript监听题型选择变化,显示/隐藏对应字段区域:

<!-- templates/add_question.html -->
<form method="post" enctype="multipart/form-data">
    {% csrf_token %}
    <div class="form-group">
        <label for="question_type">选择题型:</label>
        <select id="question_type" name="question_type" class="form-control">
            <option value="">请选择题型</option>
            <option value="MCQ">选择题</option>
            <option value="SAQ">简答题</option>
            <option value="IVQ">音视频题</option>
        </select>
    </div>

    <!-- 通用题干字段 -->
    <div class="form-group">
        <label for="question_text">题干:</label>
        <input type="text" id="question_text" name="question_text" class="form-control" required>
    </div>

    <!-- 选择题选项区域 -->
    <div id="mcq_fields" class="hidden">
        <h4>选择题选项</h4>
        {{ formset.management_form }}
        {% for form in formset %}
            <div class="option-group mb-3">
                {{ form.option_text.label_tag }}
                {{ form.option_text }}
                {{ form.is_correct.label_tag }}
                {{ form.is_correct }}
                {{ form.id }}
                {% if form.DELETE %}
                    {{ form.DELETE }} <span class="text-muted">删除</span>
                {% endif %}
            </div>
        {% endfor %}
    </div>

    <!-- 简答题区域 -->
    <div id="saq_fields" class="hidden">
        <div class="form-group">
            <label for="answer_text">答案:</label>
            <textarea id="answer_text" name="answer_text" class="form-control" required></textarea>
        </div>
        <div class="form-group">
            <label for="discussion">解析(可选):</label>
            <textarea id="discussion" name="discussion" class="form-control"></textarea>
        </div>
    </div>

    <!-- 音视频题区域 -->
    <div id="ivq_fields" class="hidden">
        <div class="form-group">
            <label for="media_file">音视频文件:</label>
            <input type="file" id="media_file" name="media_file" class="form-control">
        </div>
        <div class="form-group">
            <label for="ivq_answer">答案:</label>
            <textarea id="ivq_answer" name="answer_text" class="form-control" required></textarea>
        </div>
        <div class="form-group">
            <label for="ivq_discussion">解析(可选):</label>
            <textarea id="ivq_discussion" name="discussion" class="form-control"></textarea>
        </div>
    </div>

    <button type="submit" class="btn btn-primary mt-3">保存题目</button>
</form>

<style>
    .hidden {
        display: none;
    }
</style>

<script>
    const questionTypeSelect = document.getElementById('question_type');
    const mcqFields = document.getElementById('mcq_fields');
    const saqFields = document.getElementById('saq_fields');
    const ivqFields = document.getElementById('ivq_fields');

    function hideAllFields() {
        mcqFields.classList.add('hidden');
        saqFields.classList.add('hidden');
        ivqFields.classList.add('hidden');
    }

    questionTypeSelect.addEventListener('change', function() {
        hideAllFields();
        const selectedType = this.value;
        if (selectedType === 'MCQ') {
            mcqFields.classList.remove('hidden');
        } else if (selectedType === 'SAQ') {
            saqFields.classList.remove('hidden');
        } else if (selectedType === 'IVQ') {
            ivqFields.classList.remove('hidden');
        }
    });
</script>

三、注意事项

  • 音视频文件上传需确保Django配置了正确的媒体文件路径和访问权限
  • 选择题选项的FormSet需在视图中正确初始化,确保关联到对应的题目实例
  • 可通过AJAX动态加载FormSet的额外选项,进一步提升用户体验

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 18:04:53