Django非Admin场景下多级动态嵌套表单集实现问题求助
问题背景
Django原生inline formset仅支持3级嵌套,需实现全动态多级嵌套(每级支持一对一/一对多关联)。已定义多层关联模型,尝试多个第三方库(django-nested-inline-formsets-example、Django-better forms、django-nested-inline)均不满足需求:要么仅支持3级嵌套,要么不支持表单集,要么仅适配Admin页面。自行实现BaseMultiFormSet基类、多层嵌套表单集及SrtsCreateView视图后,仅能保存Srts表单,嵌套表单集无法通过验证完成保存。
模型代码
class Srts(models.Model): data = models.CharField(max_length=10, blank=True, null=True) class Volume(models.Model): srts = models.ForeignKey('Srts', on_delete=models.CASCADE) name = models.CharField(max_length=120, blank=True, null=True) class Qtree(models.Model): volume = models.ForeignKey('Volume', on_delete=models.CASCADE) name = models.CharField(max_length=120) class Server(models.Model): qtree = models.ForeignKey('Qtree', on_delete=models.CASCADE) hostname = models.CharField(max_length=120, blank=True, null=True) class CifsPermission(models.Model): qtree = models.ForeignKey('Qtree', on_delete=models.CASCADE) group = models.CharField(max_length=30, blank=True, null=True, default='None') permission = models.CharField(max_length=30, blank=True, null=True, default='None')
自定义BaseMultiFormSet类代码
class BaseMultiFormSet(BaseInlineFormSet): inlines = [{'inline_prefix': 'inline_formset'}] def add_nested_formsets(self, form): for inline in self.inlines: (inline_prefix, inline_formset), = inline.items() setattr( form, f'nested_{inline_prefix}', inline_formset( instance=form.instance, data=form.data if form.is_bound else None, files=form.files if form.is_bound else None, prefix=self.get_nested_prefix(inline_prefix, form.prefix, inline_formset), ) ) def add_fields(self, form, index): super().add_fields(form, index) self.add_nested_formsets(form) def get_nested_prefix(self, inline_prefix, formset_prefix, inline_formset): return f'{inline_prefix}-{formset_prefix}-{inline_formset.get_default_prefix()}' def is_valid(self): result = super().is_valid() if self.is_bound: for form in self.forms: for inline in self.inlines: (inline_prefix, inline_formset), = inline.items() if hasattr(form, f'nested_{inline_prefix}'): result = result and getattr(form, f'nested_{inline_prefix}').is_valid() return result def save(self, commit=True): result = super().save(commit=commit) for form in self.forms: for inline in self.inlines: (inline_prefix, inline_formset), = inline.items() if hasattr(form, f'nested_{inline_prefix}'): if not self._should_delete_form(form): getattr(form, f'nested_{inline_prefix}').save(commit=commit) return result def _is_adding_nested_inlines_to_empty_form(self, form, inline_prefix): if not hasattr(form, f'nested_{inline_prefix}'): return False if is_form_persisted(form): return False if not is_empty_form(form): return False non_deleted_forms = set(getattr(form, f'nested_{inline_prefix}').forms).difference( set(getattr(form, f'nested_{inline_prefix}').deleted_forms) ) return any(not is_empty_form(nested_form) for nested_form in non_deleted_forms) def clean(self): super().clean() for form in self.forms: for inline in self.inlines: (inline_prefix, inline_formset), = inline.items() if not hasattr(form, f'nested_{inline_prefix}') or self._should_delete_form(form): continue if self._is_adding_nested_inlines_to_empty_form(form, inline_prefix): form.add_error( field=None, error=( f"You are trying to add {inline_prefix}(s) to a parent which does not yet exist." ), )
嵌套表单集定义代码
UserMappingFormset = inlineformset_factory( Cifs, UserMapping, #formset=BaseQtreeFormset, form=UserMappingForm, fields='__all__', extra=2 ) CifsPermissionFormset = inlineformset_factory( Cifs, CifsPermission, #formset=BaseQtreeFormset, form = CifsPermissionForm, fields='__all__', extra=2 ) class BaseCifsFormset(BaseMultiFormSet): inlines = [ {'user_mapping': UserMappingFormset}, {'cifs_permission':CifsPermissionFormset}, ] CifsFormset = inlineformset_factory( Qtree, Cifs, form=CifsForm, formset=BaseCifsFormset, fields='__all__', extra=1 ) NfsFormset = inlineformset_factory( Qtree, Nfs, form=NfsForm, #formset=BaseQtreeFormset, fields='__all__', extra=3 ) class BaseQtreeFormset(BaseMultiFormSet): inlines = [ {'nfs': NfsFormset}, {'cifs': CifsFormset}, ] QtreeFormset = inlineformset_factory( Volume, Qtree, form=QtreeForm, formset=BaseQtreeFormset, fields='__all__', extra=1 ) class BaseVolumesFormset(BaseMultiFormSet): inlines = [ {'qtree': QtreeFormset}, ] VolumeFormset = inlineformset_factory( Srts, Volume, form=VolumeForm, formset=BaseVolumesFormset, fields= '__all__', extra=1, # If you don't want to be able to delete: # can_delete=False )
视图代码
class SrtsCreateView(CreateView): model = Srts form_class = SrtsForm template_name = 'srts/srts_create.html' success_url = reverse_lazy('srts:srts-list') def get_volume_formset(self, data=None, instance=None): return VolumeFormset(data=data, instance=instance) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['srts_form'] = context.pop('form') if self.request.POST: context['volume_forms'] = self.get_volume_formset(self.request.POST) else: context['volume_forms'] = self.get_volume_formset() return context def post(self, request, *args, **kwargs): self.object = None context = self.get_context_data(**kwargs) srts_form = context['srts_form'] volume_forms = context['volume_forms'] if srts_form.is_valid(): return self.form_valid(srts_form, volume_forms, **kwargs) else: return self.form_invalid(srts_form, volume_forms, **kwargs) def form_valid(self, srts_form, volume_forms, **kwargs): with transaction.atomic(): self.object = srts_form.save() volume_forms.instance = self.object if volume_forms.is_valid(): volume_forms.save() else: self.form_invalid(srts_form, volume_forms, **kwargs) return redirect(self.get_success_url()) def form_invalid(self, srts_form, volume_forms,**kwargs): context = self.get_context_data(**kwargs) context['volume_forms']=volume_forms return self.render_to_response(context)
问题排查与解决方案
1. 表单前缀匹配修复
当前get_nested_prefix生成的前缀格式与模板渲染的字段前缀不匹配,导致嵌套表单集无法正确读取POST数据。修改前缀生成逻辑,对齐Django原生规则:
def get_nested_prefix(self, inline_prefix, form_prefix, inline_formset): # 使用父表单前缀+子表单集默认前缀的原生格式 return f'{form_prefix}-{inline_formset.get_default_prefix()}'
同时模板中渲染嵌套表单时,必须使用对应表单集的prefix属性,例如:{{ form.nested_qtree.prefix }}。
2. 表单集绑定时机修正
form_valid中直接修改volume_forms.instance后调用is_valid()无效,因为表单集已在get_context_data中绑定了POST数据。需重新初始化表单集:
def form_valid(self, srts_form, volume_forms, **kwargs): with transaction.atomic(): self.object = srts_form.save() # 重新绑定instance和POST数据,生成新的表单集 volume_forms = self.get_volume_formset(data=self.request.POST, instance=self.object) if volume_forms.is_valid(): volume_forms.save() else: return self.form_invalid(srts_form, volume_forms, **kwargs) return redirect(self.get_success_url())
3. 嵌套表单clean逻辑调整
_is_adding_nested_inlines_to_empty_form方法会误阻止新创建的父表单绑定子表单,修改后允许绑定POST提交的新表单:
def _is_adding_nested_inlines_to_empty_form(self, form, inline_prefix): if not hasattr(form, f'nested_{inline_prefix}'): return False # 仅阻止未绑定的空父表单添加子表单,提交的新表单允许绑定 if form.is_bound: return False if is_form_persisted(form): return False if not is_empty_form(form): return False non_deleted_forms = set(getattr(form, f'nested_{inline_prefix}').forms).difference( set(getattr(form, f'nested_{inline_prefix}').deleted_forms) ) return any(not is_empty_form(nested_form) for nested_form in non_deleted_forms)
4. 模板渲染规范
确保模板完整渲染所有嵌套表单集及对应的management_form,示例结构:
<form method="post"> {% csrf_token %} {{ srts_form.as_p }} {{ volume_forms.management_form }} {% for form in volume_forms %} {{ form.as_p }} {{ form.nested_qtree.management_form }} {% for qtree_form in form.nested_qtree %} {{ qtree_form.as_p }} {{ qtree_form.nested_cifs.management_form }} {% for cifs_form in qtree_form.nested_cifs %} {{ cifs_form.as_p }} {{ cifs_form.nested_user_mapping.management_form }} {% for um_form in cifs_form.nested_user_mapping %} {{ um_form.as_p }} {% endfor %} {{ cifs_form.nested_cifs_permission.management_form }} {% for cp_form in cifs_form.nested_cifs_permission %} {{ cp_form.as_p }} {% endfor %} {% endfor %} {{ qtree_form.nested_nfs.management_form }} {% for nfs_form in qtree_form.nested_nfs %} {{ nfs_form.as_p }} {% endfor %} {% endfor %} {% endfor %} <button type="submit">保存</button> </form>
5. 验证错误排查
若仍无法通过验证,在form_invalid中打印所有层级的错误信息定位问题:
def form_invalid(self, srts_form, volume_forms,**kwargs): print("Srts表单错误:", srts_form.errors) print("Volume表单集错误:", volume_forms.errors) for vol_form in volume_forms: print(f"Volume表单{vol_form.prefix}错误:", vol_form.errors) if hasattr(vol_form, 'nested_qtree'): print(f"Qtree表单集错误:", vol_form.nested_qtree.errors) for qt_form in vol_form.nested_qtree: print(f"Qtree表单{qt_form.prefix}错误:", qt_form.errors) # 按需打印更深层级错误 context = self.get_context_data(**kwargs) context['volume_forms']=volume_forms return self.render_to_response(context)
内容的提问来源于stack exchange,提问作者Tomas Kazatel

