Django单表单多组联动下拉框实现及参数存储问题咨询
问题根因
- 模板中重复渲染同一套表单字段,所有组的输入框
id、name属性完全重复,smart_selects的联动逻辑默认只绑定第一个匹配id的元素,导致后续组联动失效 - 模型直接将单组束流参数写在主表中,不支持多组参数存储,手动存数组的方案不符合关系数据库设计规范,后续查询维护难度极高
方案实现步骤
1. 重构模型层(采用一对多关联存储多组束流参数)
修改models.py如下:
from django.db import models from smart_selects.db_fields import ChainedForeignKey class IonSpecies(models.Model): Name = models.CharField(max_length=10) def __str__(self): return self.Name class Energys(models.Model): Ion_Species = models.ForeignKey(IonSpecies, on_delete=models.CASCADE) Name = models.CharField(max_length=50) def __str__(self): return self.Name # 主表:束流请求基础信息 class BeamRequest(models.Model): #status choices SELECT = 'Select' REQUEST = 'Request' TENTATIVE = 'Tentative' ACCEPTED = 'Accepted' CONFIRMED = 'Confirmed' CANCELLED = 'Cancelled' COMPLETED = 'Completed' QUEUED = 'Queued' STATUS_CHOICES = [ (SELECT, 'Select an option'), (REQUEST, 'Request'), (TENTATIVE, 'Tentative'), (ACCEPTED, 'Accepted'), (CONFIRMED, 'Confirmed'), (CANCELLED, 'Cancelled'), (COMPLETED, 'Completed'), (QUEUED, 'Queued'), ] DIFBEAMS_CHOICES = [tuple([x,x]) for x in range(1,5)] SHIFTS_CHOICES = [tuple([x,x]) for x in range(1,17)] Project_Code = models.CharField(unique=True, max_length=20, blank = True) Pac_Rate = models.CharField(max_length=10, blank = True) Partrec_Contact_Name = models.CharField(max_length=50, blank = True) Partrec_Contact_Email = models.EmailField(blank = True) Previous_Experiment = models.CharField(max_length=200, blank = True) Status = models.CharField(max_length=25, choices=STATUS_CHOICES, default='SELECT') Project_Title = models.CharField(max_length=100, blank = True) Spokesperson_Name = models.CharField(max_length=50, blank = True) Spokesperson_Adress = models.CharField(max_length=50, blank = True) Spokesperson_Phonenumber = models.CharField(max_length=20, blank = True) Spokesperson_Email = models.EmailField(blank = True) Collaborator_Name = models.TextField(blank = True) Collaborator_Nationality = models.TextField(blank = True) Collaborator_Home_Institute = models.TextField(blank = True) Different_Beams = models.IntegerField(choices=DIFBEAMS_CHOICES, default=1) Start_Date = models.DateTimeField(blank = True, null=True) End_Date = models.DateTimeField(blank = True, null=True) Requiered_Equipment = models.TextField(blank = True) Special_Requirements = models.TextField(blank = True) Special_Safety_Procedures = models.TextField(blank = True) Lab_Support_Requirements = models.TextField(blank = True) Funded = models.TextField(blank = True) Summary = models.TextField(blank = True) def __str__(self): return self.Project_Code # 子表:单组束流参数,一个请求对应多组 class BeamParameter(models.Model): request = models.ForeignKey(BeamRequest, related_name='beams', on_delete=models.CASCADE) Hours = models.IntegerField(default=1) Ion_Species = models.ForeignKey(IonSpecies, on_delete=models.CASCADE) Energy = ChainedForeignKey(Energys, chained_field="Ion_Species", chained_model_field="Ion_Species", show_all=False, auto_choose=True) Flux = models.CharField(max_length=50, blank = True, null = True)
2. 表单层使用Formset处理多组束流参数
新建/修改forms.py:
from django import forms from django.forms import formset_factory from .models import BeamRequest, BeamParameter # 主请求表单 class BeamRequestForm(forms.ModelForm): class Meta: model = BeamRequest exclude = [] # 单组束流参数表单 class BeamParameterForm(forms.ModelForm): class Meta: model = BeamParameter fields = ['Hours', 'Ion_Species', 'Energy', 'Flux'] # 束流参数表单集,最多4组 BeamParameterFormSet = formset_factory(BeamParameterForm, extra=4, max_num=4, can_delete=False)
3. 修改模板(唯一id + 动态联动适配)
你之前贴的是模板文件不是forms.py,修改如下:
{% extends 'base.html' %} {% load crispy_forms_tags %} {% load static %} {% block extrahead %} <!-- 加载smart_selects required JS 保证联动正常 --> <script type="text/javascript" src="{% static 'smart-selects/admin/js/chainedfk.js' %}"></script> {% endblock %} {% block title %}{{ title }}{% endblock %} {% block content %} <form method="post" novalidate> {% csrf_token %} <div class="Error_Messages"> {% for error in form.non_field_errors %} {{ error }} {% endfor %} {% for error in formset.non_form_errors %} {{ error }} {% endfor %} </div> <br><br> <b>Internal contact information</b> <hr class="divider"> <div class="row"> <div class="col-6"> {{ form.Project_Code|as_crispy_field }} </div> <div class="col-6"> {{ form.Partrec_Contact_Name|as_crispy_field }} </div> </div> <div class="row"> <div class="col-6"> {{ form.Pac_Rate|as_crispy_field }} </div> <div class="col-6"> {{ form.Partrec_Contact_Email|as_crispy_field }} </div> </div> <div class="row"> <div class="col-6"> {{ form.Previous_Experiment|as_crispy_field }} </div> </div> <div class="row"> <div class="col-6"> {{ form.Status|as_crispy_field }} </div> </div> <br><br> <b>External contact information</b> <hr class="divider"> <div class="row justify-content-center"> <div class="col-6"> {{ form.Project_Title|as_crispy_field }} </div> </div> <div class="row"> <div class="col-6"> {{ form.Spokesperson_Name|as_crispy_field }} </div> <div class="col-6"> {{ form.Spokesperson_Adress|as_crispy_field }} </div> </div> <div class="row"> <div class="col-6"> {{ form.Spokesperson_Phonenumber|as_crispy_field }} </div> <div class="col-6"> {{ form.Spokesperson_Email|as_crispy_field }} </div> </div> <div class="row"> <div class="col-4"> {{ form.Collaborator_Name|as_crispy_field }} </div> <div class="col-4"> {{ form.Collaborator_Nationality|as_crispy_field }} </div> <div class="col-4"> {{ form.Collaborator_Home_Institute|as_crispy_field }} </div> </div> <br><br> <b>Beam information</b> <hr class="divider"> <div class="row"> <div class="col-6"> {{ form.Start_Date|as_crispy_field }} </div> <div class="col-6"> {{ form.End_Date|as_crispy_field }} </div> </div> <div class="justify-content-center"> <div class="col-6"> {{ form.Different_Beams|as_crispy_field }} </div> </div> <div class="row justify-content-center beam-formset"> <!-- 渲染表单集 每个表单的id自动带前缀唯一标识 --> {{ formset.management_form }} {% for beam_form in formset %} <div class="col-6 beam-group" data-index="{{ forloop.counter0 }}"> {{ beam_form|crispy }} <hr> </div> {% endfor %} </div> <div class="row justify-content-center"> {{ form.Requiered_Equipment|as_crispy_field }} {{ form.Special_Requirements|as_crispy_field }} {{ form.Special_Safety_Procedures|as_crispy_field }} {{ form.Lab_Support_Requirements|as_crispy_field }} {{ form.Funded|as_crispy_field }} {{ form.Summary|as_crispy_field }} </div> <div class="row justify-content-center"> <button type="submit" class="btn btn-success">Save New Request</button> </div> </form> <br> <script type="text/javascript"> // 初始隐藏所有束流表单,默认只显示第1组 $('.beam-group').hide(); $('.beam-group:eq(0)').show(); $('#id_Different_Beams').on('change', function() { let count = parseInt($(this).val()); $('.beam-group').hide(); // 显示对应数量的表单 for(let i=0; i<count; i++){ $(`.beam-group[data-index="${i}"]`).show(); } // 重新初始化smart_selects联动 initChainedFK(); }); </script> {% endblock %}
4. 修改视图层处理多组数据提交
修改views.py:
from django.http import Http404, HttpResponse from django.shortcuts import render, get_object_or_404, redirect from django.contrib.admin.views.decorators import staff_member_required from .models import BeamRequest from .forms import BeamRequestForm, BeamParameterFormSet def beam_request_home_page(request): page_title = 'Beam Request' template_name = 'request/home.html' context = {"title": page_title} return render(request, template_name, context) @staff_member_required def beam_request_create_page(request): page_title = 'Create new Request' template_name = 'request/create.html' if request.method == 'POST': form = BeamRequestForm(request.POST) formset = BeamParameterFormSet(request.POST) if form.is_valid() and formset.is_valid(): # 先保存主请求 req = form.save() # 保存所有束流参数 for beam_form in formset: # 跳过未显示的空表单 if beam_form.cleaned_data: beam = beam_form.save(commit=False) beam.request = req beam.save() return redirect('beam_request_home_page') else: form = BeamRequestForm() formset = BeamParameterFormSet() context = {"title": page_title, "form": form, "formset": formset} return render(request, template_name, context)
额外说明
- 表单集中每个表单的字段会自动生成唯一id,格式为
id_form-0-Ion_Species、id_form-1-Ion_Species,不会出现id重复导致的联动失效问题 - 一对多的存储结构后续查询、修改单组束流参数更方便,比直接存JSON数组更符合Django ORM的使用习惯
内容的提问来源于stack exchange,提问作者Jeroen
相关产品推荐
相关产品推荐

