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

Django为ModelForm添加子表单集实现项目材料关联及数量配置

解决方案:用Django Formset实现项目关联带数量的材料

核心思路

利用Django的inlineformset_factory创建关联Project和ProjectMaterialSet的表单集,每个子表单对应一个用户材料,包含选择复选框、隐藏的材料ID和数量输入框,实现批量处理材料关联与数量设置。


1. 修正表单代码

forms.py

from django import forms
from .models import Project, ProjectMaterialSet, Material
from django.forms import inlineformset_factory

class ProjectForm(forms.ModelForm):
    class Meta:
        model = Project
        fields = ('name', 'description')
        widgets = {
            'name': forms.TextInput(attrs={'class': 'form-control', 'placeholder': '输入项目名称'}),
            'description': forms.Textarea(attrs={'class': 'form-control', 'placeholder': '输入项目描述', 'rows': '5'})
        }

    def __init__(self, *args, **kwargs):
        self.user = kwargs.pop('user', None)
        super().__init__(*args, **kwargs)

class ProjectMaterialSetForm(forms.ModelForm):
    # 新增复选框,标记是否选择该材料
    selected = forms.BooleanField(required=False, label='选择')

    class Meta:
        model = ProjectMaterialSet
        fields = ('material', 'material_qty')
        widgets = {
            'material': forms.HiddenInput(),  # 隐藏材料ID,避免用户修改
            'material_qty': forms.NumberInput(attrs={'class': 'form-control', 'min': '1'}),
        }

    def __init__(self, *args, **kwargs):
        self.user = kwargs.pop('user', None)
        super().__init__(*args, **kwargs)
        # 编辑场景下,已关联的材料默认选中
        if self.instance.pk:
            self.fields['selected'].initial = True
        # 限制材料只能是当前用户创建的
        if self.user:
            self.fields['material'].queryset = Material.objects.filter(created_by=self.user)

    def clean(self):
        """自定义验证:选中材料时必须填写有效数量"""
        cleaned_data = super().clean()
        selected = cleaned_data.get('selected')
        material_qty = cleaned_data.get('material_qty')

        if selected:
            if not material_qty:
                self.add_error('material_qty', '选中材料后必须填写数量')
            elif material_qty < 1:
                self.add_error('material_qty', '数量必须大于0')
        return cleaned_data

# 创建关联Project和ProjectMaterialSet的inline表单集
ProjectMaterialSetFormSet = inlineformset_factory(
    Project,
    ProjectMaterialSet,
    form=ProjectMaterialSetForm,
    extra=0,  # 不自动生成额外表单,手动添加用户材料
    can_delete=False,
)

2. 视图处理逻辑

views.py

from django.shortcuts import render, redirect
from .forms import ProjectForm, ProjectMaterialSetFormSet
from .models import Material, Project

def create_project(request):
    if request.method == 'POST':
        project_form = ProjectForm(request.POST, user=request.user)
        formset = ProjectMaterialSetFormSet(request.POST, instance=project_form.instance, user=request.user)

        if project_form.is_valid() and formset.is_valid():
            # 保存项目基本信息
            project = project_form.save(commit=False)
            project.created_by = request.user
            project.save()

            # 保存选中的材料关联
            for form in formset:
                if form.cleaned_data.get('selected'):
                    material_set = form.save(commit=False)
                    material_set.project = project
                    material_set.save()

            return redirect('project_list')  # 替换为你的项目列表路由

    # GET请求:初始化表单和表单集
    project_form = ProjectForm(user=request.user)
    user_materials = Material.objects.filter(created_by=request.user)
    formset = ProjectMaterialSetFormSet(instance=project_form.instance, queryset=Material.objects.none())

    # 为每个用户材料添加一个表单实例
    for material in user_materials:
        form = ProjectMaterialSetForm(initial={'material': material}, user=request.user)
        formset.forms.append(form)

    return render(request, 'create_project.html', {
        'project_form': project_form,
        'formset': formset,
    })

def edit_project(request, pk):
    project = Project.objects.get(pk=pk, created_by=request.user)
    if request.method == 'POST':
        project_form = ProjectForm(request.POST, instance=project, user=request.user)
        formset = ProjectMaterialSetFormSet(request.POST, instance=project, user=request.user)

        if project_form.is_valid() and formset.is_valid():
            project_form.save()

            # 处理材料关联:删除未选中的,保存/更新选中的
            selected_material_pks = set()
            for form in formset:
                if form.cleaned_data.get('selected'):
                    material_pk = form.cleaned_data['material'].pk
                    selected_material_pks.add(material_pk)
                    material_set = form.save(commit=False)
                    material_set.project = project
                    material_set.save()

            # 删除未选中的关联记录
            project.projectmaterialset_set.exclude(material__pk__in=selected_material_pks).delete()

            return redirect('project_list')

    # GET请求:初始化表单和表单集,填充已有数据
    project_form = ProjectForm(instance=project, user=request.user)
    user_materials = Material.objects.filter(created_by=request.user)
    formset = ProjectMaterialSetFormSet(instance=project, queryset=Material.objects.none())

    for material in user_materials:
        # 检查是否已有关联记录,有则用实例初始化,无则用默认值
        try:
            material_set = project.projectmaterialset_set.get(material=material)
            form = ProjectMaterialSetForm(instance=material_set, user=request.user)
        except ProjectMaterialSet.DoesNotExist:
            form = ProjectMaterialSetForm(initial={'material': material}, user=request.user)
        formset.forms.append(form)

    return render(request, 'edit_project.html', {
        'project_form': project_form,
        'formset': formset,
    })

3. 模板渲染

create_project.html(编辑模板类似)

<form method="post" class="mt-4">
    {% csrf_token %}
    <!-- 项目基本信息表单 -->
    <div class="mb-4">
        {{ project_form.name.label_tag }}
        {{ project_form.name }}
        {% if project_form.name.errors %}
            <small class="text-danger d-block">{{ project_form.name.errors.0 }}</small>
        {% endif %}
    </div>
    <div class="mb-4">
        {{ project_form.description.label_tag }}
        {{ project_form.description }}
        {% if project_form.description.errors %}
            <small class="text-danger d-block">{{ project_form.description.errors.0 }}</small>
        {% endif %}
    </div>

    <!-- 材料关联表单集 -->
    <h3 class="mb-3">关联材料</h3>
    {{ formset.management_form }}  <!-- 必须渲染,Django用来识别表单集 -->
    {% for form in formset %}
        <div class="card mb-3 p-3">
            <div class="d-flex align-items-center mb-2">
                {{ form.selected }}
                <label class="ms-2 mb-0">{{ form.instance.material.name }} (${{ form.instance.material.price }}/{{ form.instance.material.unit }})</label>
                {{ form.material }}  <!-- 隐藏的材料ID字段 -->
            </div>
            <div class="col-3">
                {{ form.material_qty.label_tag }}
                {{ form.material_qty }}
                {% if form.material_qty.errors %}
                    <small class="text-danger d-block">{{ form.material_qty.errors.0 }}</small>
                {% endif %}
            </div>
        </div>
    {% empty %}
        <p class="text-muted">暂无可用材料,请先创建材料</p>
    {% endfor %}

    <button type="submit" class="btn btn-primary">保存项目</button>
</form>

关键说明

  1. 表单集初始化:通过手动添加表单实例,确保每个用户材料都对应一个表单,避免extra参数生成多余表单。
  2. 验证逻辑:在ProjectMaterialSetForm的clean方法中,确保选中材料时数量有效。
  3. 保存逻辑:创建/编辑时只处理选中的材料,自动清理未选中的关联记录(编辑场景)。

内容的提问来源于stack exchange,提问作者Fernando Velasco Borea

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 08:46:06