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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.26 02:36:03