Django同一表单创建Recipe父模型与RecipeIngredient子模型的实现方法
解决方案
你需要的功能有两种成熟实现方式,适配不同的技术栈需求:
方案一:使用Django原生InlineFormSet(无需额外前端交互,一次性提交)
这是Django官方针对一对多模型同表单提交的标准解决方案,会自动处理「先保存父模型生成主键、再自动关联子模型外键」的逻辑,完全解决你担心的主键未生成问题。
步骤1:修改forms.py添加内联表单集
from django.forms import inlineformset_factory # 原有表单代码不变,新增以下内容 # 第一个参数为父模型,第二个为子模型,extra为默认显示的空白食材输入行数 RecipeIngredientFormSet = inlineformset_factory( Recipe, RecipeIngredient, form=RecipeIngredientForm, extra=3, can_delete=True )
步骤2:修改recipe_create_view视图处理表单集提交
@login_required def recipe_create_view(request): # 记得加request.FILES处理图片上传 form = RecipeForm(request.POST or None, request.FILES or None) formset = RecipeIngredientFormSet(request.POST or None) context = { "form": form, "formset": formset } if form.is_valid() and formset.is_valid(): obj = form.save(commit=False) obj.user = request.user obj.save() # 子模型表单集关联刚生成的食谱主键,批量保存食材 formset.instance = obj formset.save() if request.htmx: headers = { "HX-Redirect": obj.get_absolute_url() } return HttpResponse("Created", headers=headers) return redirect(obj.get_absolute_url()) return render(request, "recipes/create.html", context)
步骤3:修改create.html模板渲染食材表单
找到模板中<!-- ADD INGREDIENTS ? -->的位置,替换为以下内容:
<h3>食材清单</h3> <!-- 这行是Django表单集的配置字段,必须保留 --> {{ formset.management_form }} {% for ingredient_form in formset %} <div class="ingredient-row row pb-2"> <div class="col-4">{{ ingredient_form.name|as_crispy_field }}</div> <div class="col-3">{{ ingredient_form.quantity|as_crispy_field }}</div> <div class="col-3">{{ ingredient_form.unit|as_crispy_field }}</div> <div class="col-2 pt-4"> {% if ingredient_form.instance.pk %}{{ ingredient_form.DELETE }}{% endif %} </div> </div> {% endfor %} <!-- 可选:添加按钮动态新增食材行 --> <button type="button" id="add-ingredient" class="btn btn-outline-secondary btn-sm mt-2">+ 新增食材</button>
如果需要动态新增食材行,在模板末尾添加一段简单JS即可:
<script> document.getElementById('add-ingredient').addEventListener('click', function() { const formsetDiv = document.querySelector('.ingredient-row').parentNode; const totalForms = document.getElementById('id_form-TOTAL_FORMS'); const currentCount = parseInt(totalForms.value); const newRow = document.querySelector('.ingredient-row').cloneNode(true); // 更新新行的表单字段索引 newRow.innerHTML = newRow.innerHTML.replace(/form-\d+/g, `form-${currentCount}`); // 清空新行的输入值 newRow.querySelectorAll('input, select').forEach(input => input.value = ''); formsetDiv.appendChild(newRow); totalForms.value = currentCount + 1; }); </script>
方案二:基于现有HTMX技术栈实现(分步提交,用户体验更好)
你已经写好了食材的HTMX增删改接口,只需要调整创建逻辑,先异步保存食谱草稿拿到ID,再加载食材录入区域即可,改动量非常小。
步骤1:修复urls.py的笔误
你现有create路由多了一个多余的大于号,修改为:
path('create/', recipe_create_view, name='create'),
步骤2:修改创建视图支持草稿提交
@login_required def recipe_create_view(request): form = RecipeForm(request.POST or None, request.FILES or None) context = { "form": form, } if form.is_valid(): obj = form.save(commit=False) obj.user = request.user obj.save() if request.htmx: # 如果是草稿提交,返回食谱ID和食材录入区域 if request.GET.get('draft'): context = { "recipe_id": obj.id, "object": obj } return render(request, "recipes/partials/ingredient-section.html", context) # 最终提交直接跳转详情页 headers = { "HX-Redirect": obj.get_absolute_url() } return HttpResponse("Created", headers=headers) return redirect(obj.get_absolute_url()) return render(request, "recipes/create.html", context)
步骤3:新增食材区域模板ingredient-section.html
<div id="ingredient-section"> <h3>食材清单</h3> <div id="ingredient-list"> <!-- 渲染已添加的食材 --> {% for ingredient in object.get_ingredients_children %} {% include "recipes/partials/ingredient-inline.html" with object=ingredient edit=True %} {% endfor %} </div> <!-- 新增食材按钮,复用你已有的HTMX接口 --> <button class="btn btn-outline-secondary btn-sm mt-2" hx-get="{% url 'recipes:hx-ingredient-create' parent_id=recipe_id %}" hx-target="#ingredient-list" hx-swap="beforeend"> + 新增食材 </button> <!-- 最终保存按钮 --> <button class="btn btn-success mt-3" type="submit">保存整个食谱</button> </div>
步骤4:修改create.html模板调整提交逻辑
把原来的保存按钮替换为草稿提交按钮,修改后代码如下:
<div class="d-flex"> <button class="btn btn-primary htmx-inverted-indicator" style='margin-top:10px;' type="button" hx-post=".?draft=1" hx-target="#ingredient-wrapper" hx-swap="innerHTML"> 先保存食谱基础信息,继续加食材 </button> <a class="btn btn-danger" href='{% url "recipes:list" %}'>取消</a> </div> <!-- 食材区域占位框 --> <div id="ingredient-wrapper" class="mt-4"></div>
内容的提问来源于stack exchange,提问作者Nick
相关产品推荐
相关产品推荐

