多题型测验应用设计方案及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
相关产品推荐
相关产品推荐

