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

如何为带中间模型的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 00:53:31