如何在前端实现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

