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>
关键说明
- 表单集初始化:通过手动添加表单实例,确保每个用户材料都对应一个表单,避免
extra参数生成多余表单。 - 验证逻辑:在
ProjectMaterialSetForm的clean方法中,确保选中材料时数量有效。 - 保存逻辑:创建/编辑时只处理选中的材料,自动清理未选中的关联记录(编辑场景)。
内容的提问来源于stack exchange,提问作者Fernando Velasco Borea
相关产品推荐
相关产品推荐

