向 ModelForm 添加一组“子表单”(可能是表单集)

问题描述 投票:0回答:1

我在解决这个问题方面取得了一些进展。我想要建模的是这样的:

我有一个材料模型、一个项目模型和一个中间模型,用于为它们之间的多对多关系指定更多数据。目标是让一个项目不仅可以拥有多种材料,而且还可以指定每种材料的数量。

这些是我的模型:

class Material(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=200)
    description = models.CharField(max_length=3000, blank=True)
    unit = models.CharField(max_length=20)
    price = models.DecimalField(max_digits=9, decimal_places=2, validators=[MinValueValidator(0)])
    created_by = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='materials')
    created_on = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f'{self.name} (${self.price}/{self.unit})'


class Project(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=200)
    description = models.CharField(max_length=3000, blank=True)
    materials = models.ManyToManyField(to=Material, through='ProjectMaterialSet', related_name='projects')
    created_by = models.ForeignKey(to=User, on_delete=models.CASCADE)
    created_on = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f'{self.name}'

class ProjectMaterialSet(models.Model):
    id = models.AutoField(primary_key=True)
    project = models.ForeignKey(to=Project, on_delete=models.CASCADE)
    material = models.ForeignKey(to=Material, on_delete=models.CASCADE)
    material_qty = models.PositiveIntegerField(default=1)

现在,我已经成功渲染了项目表单,其中包含材料的复选框列表(没有数量功能)、另一个仅列出一种材料作为复选框的表单以及数量字段(ProjectMaterialSet 模型的一种表单)。

现在,我正在努力解决的问题是如何将两者结合起来。到目前为止,我认为我需要做的是在 ProjectForm 中拥有一个可用材料的查询集,然后向其中添加一系列子表单(这是我认为我应该使用表单集的地方,但是,我不确定如何)传递每个材质 pk,这样我就可以将每个表单渲染为 ModelMultipleChoiceField,这样我就可以为每种材质使用复选框小部件。

作为与表单集无关的人,我的本能是对材料进行 for 循环并创建保存到字典或围绕这些行的内容的表单实例。然而,我在 Django 论坛上读到了一个关于使用 Django 功能而不是破解解决方案的好短语,我确实觉得表单集可以用于此目的,我只是不确定如何使用。

澄清:我需要将每种材料 pk 传递到 ProjectMaterialSetForm,以便我可以获得每种材料的复选框(因为我正在按 pk 进行过滤,所以我得到的查询集只有一个结果),并将数量输入链接到那个复选框。

我觉得我已经完成了大部分工作,我只是不确定如何添加子表单,如果有人也可以帮助完成所需的自定义保存和验证,那就太棒了!

这是我的表格:

class ProjectForm(forms.ModelForm):

    class Meta:
        model = Project
        fields = ('name', 'description')

        widgets = {
            'name': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Enter the project name'}),
            'description': forms.Textarea(attrs={'class': 'form-control', 'placeholder': 'Enter the project description', 'rows': '5'})
        }

    def __init__(self, *args, **kwargs):

        # Store the user object and materials QuerySet
        self.user = kwargs.pop('user') if 'user' in kwargs else None
        self.materials = Material.objects.filter(created_by=self.user) if self.user is not None else None # This is a QuerySet

        # Call parent __init__ method, which we are modifying
        super(ProjectForm, self).__init__(*args, **kwargs)

        # Somehow create a set of forms, one for each material
            # Perhaps using formsets is the ideal solution, however, I keep finding
            # myself going to the route of a dict with forms and adding them as I
            # iterate over the materials query set. 

            # Since formsets are a thing, I'm pretty sure I can somehow create a batch
            # of forms (probably the extra argument is the key here), to then somehow
            # add the material_id to each of them.


class ProjectMaterialSetForm(forms.ModelForm):

    class Meta:
        model = ProjectMaterialSet
        fields = ('material', 'material_qty')

        material = forms.ModelMultipleChoiceField(queryset=None)
        
        widgets = {
            'material': forms.CheckboxSelectMultiple(attrs={'class':'form-check-input'}),
            'material_qty': forms.NumberInput(attrs={'class':'form-control'})
        }

    def __init__(self, *args, **kwargs):
        material_id = kwargs.pop('material_id') if 'material_id' in kwargs else None
        super(ProjectMaterialSetForm, self).__init__(*args, **kwargs)
        self.fields['materials'].queryset = Material.objects.filter(pk=material_id)
python django django-orm modelform formset
1个回答
0
投票

我遇到了类似的情况,最终我自定义了模型表单集。

class ProjectMaterialSetFormSet(forms.BaseModelFormSet):
    # some required / optional attributes
    extra = 0
    can_delete = True
    can_order = False
    max_num = 1000
    validate_max = False
    # If you want to enforce each project to have at least
    # one material, set validate_min to True
    min_num = 1
    validate_min = False
    absolute_max = 1000
    can_delete_extra = True
    renderer = forms.renderers.get_default_renderer()

    model = ProjectMaterialSet
    class ProjectMaterialSetForm(forms.ModelForm):
        # Add an extra name field to help form rendering
        material_name = forms.CharField(max_length=200, required=False)
        class Meta:
            model = ProjectMaterialSet
            fields = ['material', 'material_qty']
    form = ProjectMaterialSetForm

    # ProjectForm is like another management form for the formset: 
    # rendered, validated and saved together with formset. 
    class ProjectForm(forms.ModelForm):
        class Meta:
            model = Project
            fields = ['name', 'description']

    @cached_property
    def project_form(self):
        if self.is_bound:
            form = self.ProjectForm(self.data, self.files, prefix=self.prefix)
            form.full_clean()
        else:
            form = self.ProjectForm(prefix=self.prefix)
        return form

    def clean(self):
        super().clean()
        if not self.project_form.is_valid():
            raise forms.ValidationError('Project form invalid')

    def save(self, commit=True):
        project = self.project_form.save(commit=commit)
        for form in self:
            form.instance.project = project
        return super(ProjectMaterialSetFormSet, self).save(commit=commit)
        # return project if you want the instance for further operation,
        # but just don't forget to call super().save()

    def get_queryset(self):
        # Override get_queryset to return none if not specified. 
        # Otherwise, it just returns all. 
        if not hasattr(self, '_queryset'):
            if self.queryset is not None:
                qs = self.queryset
            else:
                qs = self.model.objects.none()
            if not qs.ordered:
                qs = qs.order_by(self.model._meta.pk.name)
            self._queryset = qs
        return self._queryset

渲染表单集时,提供所有材质作为初始数据

def project_create_view(request):
    if request.method == 'POST':
        formset = ProjectMaterialSetFormSet(request.POST, prefix='proj-mats')
        if formset.is_valid():
            proj_mat_set = formset.save()
    else:
        initial = Material.objects.values(material=F('id'), material_name=F('name')).annotate(DELETE
        formset = ProjectMaterialSetFormSet(initial=initial)
        # Not so sure about why minus 1 here. I did it 
        # in my code. Remove it if you find problem. 
        formset.extra = len(initial) - 1
    context = {'formset': formset}
    return render(request, 'path/to/template.html', context)

渲染模板时,不要忘记渲染project_form。如果您倾向于单独渲染每个子表单(此处推荐),也不要忘记渲染管理表单。

{{ formset.project_form }}
{{ formset.management_form }}
{% for form in formset %}
<input type="checkbox" name="{{ form.prefix }}-DELETE" {{ form.DELETE.value|yesno:'checked,' }} id="id-{{ form.prefix }}-DELETE">
<input type="hidden" name="{{ form.prefix }}-material" value="{{ form.material.value }}">
<input type="hidden" name="{{ form.prefix }}-material_name" value="{{ form.material_name.value }}">
<label for="id-{{ form.prefix }}-DELETE">{{ form.material_name.value }}</label>
<input type="text" name="{{ form.prefix }}-material_qty" value="{{ form.material_qty.value }}" pattern="\d+">
{% endfor %}
© www.soinside.com 2019 - 2024. All rights reserved.