如何为带中间模型的Django Model创建表单?(如含多数量产品的盒子)
Django 实现项目(Box)关联材料(Product)及数量的表单方案
问题背景
我正在开发的场景是:用Box模型对应项目,Product模型对应材料,需要存储每个项目所需的各类材料及对应数量(比如蛋糕项目需要5个鸡蛋、1磅面粉)。当前的模型结构如下:
class Box(models.Model): boxName = models.CharField(max_length=255, blank=False) product_set = models.ManyToManyField(ProductSet) def __str__(self): return self.boxName class ProductSet(models.Model): quantity = models.PositiveIntegerField() product = models.ForeignKey(Product, on_delete=models.PROTECT) class Product(models.Model): productName = models.CharField(max_length=200) productDescription = models.TextField(blank=True) productPrice = models.DecimalField(max_digits=9, decimal_places=0, default=0) class Meta: db_table = 'products' ordering = ['-productName'] def __str__(self): return self.productName
现在的问题是:无法实现Box的ModelForm——默认的ModelMultipleChoiceField只能选择已有的ProductSet实例,但我需要在创建项目的表单里直接新增材料-数量组合,而不是选择已有的组合。
解决方案:使用表单集(FormSet)+ 自定义视图处理
1. 创建材料数量表单及表单集
首先为ProductSet创建单独表单,再通过表单集实现多组材料数量的输入:
# forms.py from django import forms from .models import Box, ProductSet, Product # 单个材料-数量的输入表单 class ProductSetForm(forms.ModelForm): class Meta: model = ProductSet fields = ['product', 'quantity'] widgets = { 'product': forms.Select(attrs={'class': 'form-control'}), 'quantity': forms.NumberInput(attrs={'class': 'form-control', 'min': 1}), } # 生成关联Box的表单集,支持多组输入 ProductSetFormSet = forms.inlineformset_factory( Box, ProductSet, form=ProductSetForm, extra=1, # 默认显示1组输入框 can_delete=True, # 允许删除不需要的输入组 )
2. 创建项目基础表单
为Box创建仅处理项目名称的基础表单:
class BoxForm(forms.ModelForm): class Meta: model = Box fields = ['boxName'] widgets = { 'boxName': forms.TextInput(attrs={'class': 'form-control'}), }
3. 编写视图处理表单提交
在视图中同时验证并保存项目表单和材料数量表单集,完成关联逻辑:
# views.py from django.shortcuts import render, redirect from .forms import BoxForm, ProductSetFormSet def create_box(request): if request.method == 'POST': box_form = BoxForm(request.POST) formset = ProductSetFormSet(request.POST) if box_form.is_valid() and formset.is_valid(): # 先保存项目但暂不提交数据库 box = box_form.save(commit=False) box.save() # 保存所有材料-数量组合并关联到当前项目 instances = formset.save(commit=False) for instance in instances: instance.save() box.product_set.add(instance) # 处理表单集的删除操作 formset.save_m2m() return redirect('box_list') # 替换为你的项目列表路由 else: box_form = BoxForm() formset = ProductSetFormSet() return render(request, 'create_box.html', { 'box_form': box_form, 'formset': formset, })
4. 编写模板实现动态输入功能
渲染表单并添加JS,让用户可以手动新增材料-数量输入组:
<!-- templates/create_box.html --> <form method="post"> {% csrf_token %} <h3>项目信息</h3> {{ box_form.as_p }} <h3>所需材料</h3> {{ formset.management_form }} <div class="form-container"> {% for form in formset %} <div class="form-group mb-3"> {{ form.id }} <div class="row"> <div class="col"> {{ form.product.label_tag }} {{ form.product }} </div> <div class="col"> {{ form.quantity.label_tag }} {{ form.quantity }} </div> {% if formset.can_delete %} <div class="col-auto"> {{ form.DELETE }} {{ form.DELETE.label_tag }} </div> {% endif %} </div> </div> {% endfor %} </div> <button type="button" id="add-material" class="btn btn-secondary mb-3">添加材料</button> <button type="submit" class="btn btn-primary">保存项目</button> </form> <script> document.addEventListener('DOMContentLoaded', function() { const totalForms = document.getElementById('id_product_set-TOTAL_FORMS'); const formContainer = document.querySelector('.form-container'); const addBtn = document.getElementById('add-material'); addBtn.addEventListener('click', function() { const formCount = parseInt(totalForms.value); // 复制现有表单结构并替换索引 const newForm = document.querySelector('.form-group').cloneNode(true); newForm.innerHTML = newForm.innerHTML.replace(/id_product_set-\d-/g, `id_product_set-${formCount}-`); // 清空输入值 newForm.querySelector('select').value = ''; newForm.querySelector('input[type="number"]').value = ''; if (newForm.querySelector('input[type="checkbox"]')) { newForm.querySelector('input[type="checkbox"]').checked = false; } formContainer.appendChild(newForm); totalForms.value = formCount + 1; }); }); </script>
5. 配置URL路由
在urls.py中添加创建项目的路由:
# urls.py from django.urls import path from .views import create_box urlpatterns = [ path('box/create/', create_box, name='create_box'), # 其他路由... ]
关键说明
- 表单集(FormSet):专门用于处理多组同类型表单的提交,
inlineformset_factory会自动关联父模型与子模型的关系。 - commit=False:延迟模型提交到数据库的时机,方便后续完成关联操作。
- 动态JS:让用户可以按需添加更多材料输入组,提升表单的实用性。
内容的提问来源于stack exchange,提问作者Fernando Velasco Borea
相关产品推荐
相关产品推荐

