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

如何在前端实现Django三级嵌套内联表单?

Django 三级堆叠内联表单实现问题

我正尝试在Django中实现三级堆叠内联表单,模型结构如下:

class Anuncio(models.Model):
    title = models.CharField(max_length=200)
    delivery = models.CharField(max_length=100)

class Product(models.Model):
    anuncio = models.ForeignKey(Anuncio, on_delete=models.CASCADE)
    name = models.CharField(max_length=200)
    quantity = models.PositiveIntegerField(default=1)
    price = models.PositiveIntegerField()

class Image(models.Model):
    product = models.ForeignKey(Product, on_delete=models.CASCADE)
    image = models.ImageField()

模型间存在Anuncio-Product和Product-Image的关联关系。已借助django-nested-admin包在Django Admin中实现需求:创建Anuncio对象时可添加任意数量的Product,每个Product又可添加任意数量的Image。现在需要在前端实现该功能。

我认为应使用Django formsets,但遇到了问题,网上找到的资源仅涉及二级表单集,或三级场景下外键均指向同一父模型的情况。

我编写了如下forms.py文件:

class ProductForm(ModelForm):
    class Meta: 
        model = Product 
        fields = ['name', 'quantity', 'price']

class ImageForm(ModelForm):
    class Meta: 
        model = Imagen  # 此处存在拼写错误,应为Image
        fields = ['image']

class AnuncioForm(ModelForm):
    class Meta: 
        model = Anuncio 
        fields = ['title', 'delivery']

以及views.py函数:

def anunciocreateview(request):
 form = AnuncioForm(request.POST or None)
 ProductFormSet = inlineformset_factory(Anuncio, Product, form=ProductForm)
 ImageFormSet = inlineformset_factory(Product, Image, form=ImageForm)
 if all([form.is_valid(), ProductFormSet.is_valid(), ImageFormSet.is_valid()]):
     parent = form.save(commit=False)
     parent.anunciante = request.user
     parent.save()
     for form in ProductoFormSet:  # 拼写错误,应为ProductFormSet
         child = form.save(commit=False)
         child.anuncio = parent 
         child.save()
     for form in ImagenFormSet:  # 拼写错误,应为ImageFormSet
         imagen = form.save(commit=False)
         imagen.product = form.product
         imagen.save()
  context = {
     'form_1' : form,
     'form_2' : ProductFormSet,
     'form_3' : ImageFormSet,
    }

但当前代码触发了AttributeError:'ProductForm' object has no attribute '__name__',而且模型关联处理上存在疏漏。另外,AdminStackedInLine中的“添加额外Product”功能,我猜测可通过JavaScript实现,借助隐藏表单和点击事件修改属性。

请问如何正确实现该功能?同时想了解如何处理提交表单的数据及关联关系?


解决方案

1. 修正视图层Formset初始化逻辑

错误根源是直接对Formset类调用is_valid(),而非实例化后的对象。三级表单集需要动态为每个Product绑定对应的Image表单集,正确代码如下:

from django.shortcuts import render
from .models import Anuncio, Product, Image
from .forms import AnuncioForm, ProductForm, ImageForm
from django.forms import inlineformset_factory

def anunciocreateview(request):
    # 初始化主表单,处理文件上传
    form = AnuncioForm(request.POST or None, request.FILES or None)
    
    # 初始化Product表单集
    ProductFormSet = inlineformset_factory(Anuncio, Product, form=ProductForm, extra=1)
    product_formset = ProductFormSet(
        request.POST or None, 
        request.FILES or None, 
        instance=Anuncio() if request.method == 'GET' else form.instance
    )
    
    # 动态生成每个Product对应的Image表单集
    image_formsets = []
    if request.method == 'POST':
        # POST请求时,根据已提交的Product数量生成对应Image表单集
        for i in range(len(product_formset)):
            ImageFormSet = inlineformset_factory(Product, Image, form=ImageForm, extra=1)
            target_product = product_formset[i].instance if product_formset[i].instance.pk else Product()
            image_formset = ImageFormSet(
                request.POST or None, 
                request.FILES or None, 
                instance=target_product,
                prefix=f'image-{i}'
            )
            image_formsets.append(image_formset)
    else:
        # GET请求时,为初始的Product表单生成Image表单集
        for i in range(product_formset.extra):
            ImageFormSet = inlineformset_factory(Product, Image, form=ImageForm, extra=1)
            image_formset = ImageFormSet(prefix=f'image-{i}')
            image_formsets.append(image_formset)
    
    # 验证所有表单和表单集
    all_valid = form.is_valid()
    all_valid &= product_formset.is_valid()
    for img_formset in image_formsets:
        all_valid &= img_formset.is_valid()
    
    if all_valid and request.method == 'POST':
        # 保存主Anuncio对象
        anuncio = form.save(commit=False)
        anuncio.anunciante = request.user
        anuncio.save()
        
        # 保存Product及对应Image
        for idx, product_form in enumerate(product_formset):
            if product_form.cleaned_data:
                product = product_form.save(commit=False)
                product.anuncio = anuncio
                product.save()
                # 保存当前Product的Image
                img_formset = image_formsets[idx]
                for img_form in img_formset:
                    if img_form.cleaned_data:
                        image = img_form.save(commit=False)
                        image.product = product
                        image.save()
    
    context = {
        'anuncio_form': form,
        'product_formset': product_formset,
        'image_formsets': image_formsets,
    }
    return render(request, 'anuncio_create.html', context)

2. 前端模板实现三级嵌套渲染

模板需渲染主表单、Product表单集,同时为每个Product绑定对应的Image表单集,并添加动态添加按钮:

<form method="post" enctype="multipart/form-data">
    {% csrf_token %}
    {{ anuncio_form.as_p }}
    
    <h3>商品列表</h3>
    {{ product_formset.management_form }}
    {% for product_form in product_formset %}
        <div class="product-container" style="border:1px solid #ddd; padding:10px; margin:10px 0;">
            {{ product_form.as_p }}
            {{ product_form.id }}
            
            <h4>商品图片</h4>
            {% with img_formset=image_formsets|get_item:forloop.counter0 %}
                {{ img_formset.management_form }}
                {% for img_form in img_formset %}
                    <div class="image-container" style="margin-left:20px;">
                        {{ img_form.as_p }}
                        {{ img_form.id }}
                    </div>
                {% endfor %}
                <button type="button" class="add-image-btn" data-prefix="{{ img_formset.prefix }}">添加图片</button>
            {% endwith %}
        </div>
    {% endfor %}
    <button type="button" class="add-product-btn">添加商品</button>
    <br><br>
    <button type="submit">保存</button>
</form>

<!-- 自定义模板过滤器,用于按索引取列表元素 -->
{% load template_filters %}

需要在template_filters.py中添加自定义过滤器:

from django import template

register = template.Library()

@register.filter
def get_item(list_data, index):
    return list_data[index]

3. JavaScript实现动态添加表单功能

通过复制表单模板、修改字段前缀和ID,实现点击按钮添加新的Product或Image:

// 动态添加商品表单
document.querySelector('.add-product-btn').addEventListener('click', function() {
    const productContainer = document.querySelector('.product-container');
    const managementForm = document.querySelector('#id_product_set-TOTAL_FORMS');
    const totalForms = parseInt(managementForm.value);
    
    // 复制现有商品表单作为模板
    const newProductForm = productContainer.cloneNode(true);
    // 更新表单内所有元素的name和id
    newProductForm.querySelectorAll('[name], [id]').forEach(el => {
        const oldName = el.name || el.id;
        const newName = oldName.replace(/product_set-\d+/, `product_set-${totalForms}`);
        el.name = newName;
        el.id = newName;
        // 清空非隐藏输入框的值
        if (el.tagName === 'INPUT' && el.type !== 'hidden') {
            el.value = '';
        }
    });
    
    // 为新商品生成对应的图片表单集
    const imgPrefix = `image-${totalForms}`;
    const imgManagementHtml = `
        <input type="hidden" name="${imgPrefix}-TOTAL_FORMS" value="1">
        <input type="hidden" name="${imgPrefix}-INITIAL_FORMS" value="0">
        <input type="hidden" name="${imgPrefix}-MIN_NUM_FORMS" value="0">
        <input type="hidden" name="${imgPrefix}-MAX_NUM_FORMS" value="1000">
    `;
    const imgFormHtml = `
        <div class="image-container" style="margin-left:20px;">
            <p><label for="${imgPrefix}-0-image">图片:</label> <input type="file" name="${imgPrefix}-0-image" id="${imgPrefix}-0-image"></p>
            <input type="hidden" name="${imgPrefix}-0-id" id="${imgPrefix}-0-id">
        </div>
    `;
    const addImageBtn = document.createElement('button');
    addImageBtn.type = 'button';
    addImageBtn.className = 'add-image-btn';
    addImageBtn.dataset.prefix = imgPrefix;
    addImageBtn.textContent = '添加图片';
    
    // 插入新生成的图片表单元素
    const imgManagementEl = document.createElement('div');
    imgManagementEl.innerHTML = imgManagementHtml;
    const imgFormEl = document.createElement('div');
    imgFormEl.innerHTML = imgFormHtml;
    
    newProductForm.querySelector('h4').after(imgManagementEl);
    newProductForm.querySelector('h4').after(imgFormEl);
    newProductForm.appendChild(addImageBtn);
    
    // 将新商品表单添加到页面
    productContainer.parentNode.appendChild(newProductForm);
    // 更新商品表单总数
    managementForm.value = totalForms + 1;
    
    // 为新的添加图片按钮绑定事件
    addImageBtn.addEventListener('click', addImageHandler);
});

// 动态添加图片表单
function addImageHandler(e) {
    const prefix = e.target.dataset.prefix;
    const managementForm = document.querySelector(`[name="${prefix}-TOTAL_FORMS"]`);
    const totalForms = parseInt(managementForm.value);
    const imageContainer = e.target.previousElementSibling;
    
    // 创建新的图片表单
    const newImgForm = document.createElement('div');
    newImgForm.className = 'image-container';
    newImgForm.style.marginLeft = '20px';
    newImgForm.innerHTML = `
        <p><label for="${prefix}-${totalForms}-image">图片:</label> <input type="file" name="${prefix}-${totalForms}-image" id="${prefix}-${totalForms}-image"></p>
        <input type="hidden" name="${prefix}-${totalForms}-id" id="${prefix}-${totalForms}-id">
    `;
    
    // 插入新表单并更新总数
    imageContainer.parentNode.insertBefore(newImgForm, e.target);
    managementForm.value = totalForms + 1;
}

// 初始化已有添加图片按钮的事件绑定
document.querySelectorAll('.add-image-btn').forEach(btn => {
    btn.addEventListener('click', addImageHandler);
});

关键注意事项

  • 表单必须添加enctype="multipart/form-data"属性,用于处理图片上传。
  • 每个Image表单集必须设置唯一的prefix,避免字段名称冲突。
  • 必须渲染每个表单集的management_form,Django依赖它识别表单数量。
  • 保存数据时需检查form.cleaned_data是否为空,避免创建空的Product或Image记录。

内容的提问来源于stack exchange,提问作者rolandist_scim

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 14:15:40