Django动态表单开发求助:基于问题类型生成对应关联字段
解决方案:Django动态题型表单实现
1. 后端表单配置(forms.py)
先定义主问题表单和关联的选项、子问题表单,再用表单集处理批量字段:
from django import forms from .models import Question, Option, Subquestion # 主问题表单(仅题型选择) class QuestionForm(forms.ModelForm): class Meta: model = Question fields = ['question_type'] # 选项子表单 class OptionForm(forms.ModelForm): class Meta: model = Option fields = ['option'] widgets = {'option': forms.TextInput(attrs={'placeholder': '输入选项内容'})} # 子问题子表单 class SubquestionForm(forms.ModelForm): class Meta: model = Subquestion fields = ['subquestion'] widgets = {'subquestion': forms.TextInput(attrs={'placeholder': '输入子问题内容'})} # 创建表单集(初始额外数量设为0,后续动态调整) OptionFormSet = forms.formset_factory(OptionForm, extra=0) SubquestionFormSet = forms.formset_factory(SubquestionForm, extra=0)
2. 视图逻辑(views.py)
处理表单提交、验证和数据保存,同时兼容验证失败后的回显:
from django.shortcuts import render, redirect from .forms import QuestionForm, OptionFormSet, SubquestionFormSet from .models import Question def create_question(request): if request.method == 'POST': question_form = QuestionForm(request.POST) if question_form.is_valid(): question = question_form.save() q_type = question.question_type # 保存选项(SBA/EMQ题型) if q_type in ['SBA', 'EMQ']: option_formset = OptionFormSet(request.POST, prefix='options') if option_formset.is_valid(): for form in option_formset: if form.cleaned_data.get('option'): option = form.save(commit=False) option.question = question option.save() # 保存子问题(仅EMQ题型) if q_type == 'EMQ': subq_formset = SubquestionFormSet(request.POST, prefix='subquestions') if subq_formset.is_valid(): for form in subq_formset: if form.cleaned_data.get('subquestion'): subq = form.save(commit=False) subq.question = question subq.save() return redirect('question_list') # 替换为你的问题列表页面URL else: # 验证失败,根据提交的题型重新初始化对应数量的表单集 q_type = request.POST.get('question_type', '') option_extra = 5 if q_type == 'SBA' else 7 if q_type == 'EMQ' else 0 subq_extra = 5 if q_type == 'EMQ' else 0 option_formset = OptionFormSet(request.POST, prefix='options', extra=option_extra) subq_formset = SubquestionFormSet(request.POST, prefix='subquestions', extra=subq_extra) else: # GET请求初始化空表单 question_form = QuestionForm() option_formset = OptionFormSet(prefix='options', extra=0) subq_formset = SubquestionFormSet(prefix='subquestions', extra=0) context = { 'question_form': question_form, 'option_formset': option_formset, 'subq_formset': subq_formset } return render(request, 'create_question.html', context)
3. 前端模板(create_question.html)
通过JS监听题型选择变化,动态生成/隐藏对应字段:
<form method="post"> {% csrf_token %} <div class="form-group"> {{ question_form.question_type.label_tag }} {{ question_form.question_type }} </div> <!-- 选项区域 --> <div id="options-section" style="display: none; margin-top: 1rem;"> <h4>选项</h4> {{ option_formset.management_form }} <div id="options-container"> {% for form in option_formset %} <div class="option-item">{{ form.option }}</div> {% endfor %} </div> </div> <!-- 子问题区域 --> <div id="subquestions-section" style="display: none; margin-top: 1rem;"> <h4>子问题</h4> {{ subq_formset.management_form }} <div id="subquestions-container"> {% for form in subq_formset %} <div class="subq-item">{{ form.subquestion }}</div> {% endfor %} </div> </div> <button type="submit" style="margin-top: 1rem;">保存问题</button> </form> <script> const typeSelect = document.getElementById('id_question_type'); const optionsSection = document.getElementById('options-section'); const subqSection = document.getElementById('subquestions-section'); const optionsContainer = document.getElementById('options-container'); const subqContainer = document.getElementById('subquestions-container'); const optionTotalForms = document.getElementById('id_options-TOTAL_FORMS'); const subqTotalForms = document.getElementById('id_subquestions-TOTAL_FORMS'); // 根据题型更新表单字段 function updateFields() { const selectedType = typeSelect.value; // 清空现有表单 optionsContainer.innerHTML = ''; subqContainer.innerHTML = ''; // 处理选项 if (selectedType === 'SBA' || selectedType === 'EMQ') { optionsSection.style.display = 'block'; const count = selectedType === 'SBA' ? 5 : 7; optionTotalForms.value = count; for (let i = 0; i < count; i++) { const item = document.createElement('div'); item.className = 'option-item'; item.innerHTML = `<input type="text" name="options-${i}-option" placeholder="选项 ${i+1}" id="id_options-${i}-option">`; optionsContainer.appendChild(item); } } else { optionsSection.style.display = 'none'; optionTotalForms.value = 0; } // 处理子问题 if (selectedType === 'EMQ') { subqSection.style.display = 'block'; const count = 5; subqTotalForms.value = count; for (let i = 0; i < count; i++) { const item = document.createElement('div'); item.className = 'subq-item'; item.innerHTML = `<input type="text" name="subquestions-${i}-subquestion" placeholder="子问题 ${i+1}" id="id_subquestions-${i}-subquestion">`; subqContainer.appendChild(item); } } else { subqSection.style.display = 'none'; subqTotalForms.value = 0; } } // 初始化+监听变化 typeSelect.addEventListener('change', updateFields); updateFields(); </script>
关键说明
- 表单集的
prefix参数必须设置,避免不同表单集的字段名冲突 - 前端通过修改
TOTAL_FORMS隐藏字段,告诉后端需要处理的表单数量 - 保存时过滤空内容,避免创建无意义的关联数据
- 验证失败时,根据提交的题型重新渲染对应数量的表单,保留用户输入
内容的提问来源于stack exchange,提问作者Shiva
相关产品推荐
相关产品推荐

