我在解决这个问题方面取得了一些进展。我想要建模的是这样的:
我有一个材料模型、一个项目模型和一个中间模型,用于为它们之间的多对多关系指定更多数据。目标是让一个项目不仅可以拥有多种材料,而且还可以指定每种材料的数量。
这些是我的模型:
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)
我遇到了类似的情况,最终我自定义了模型表单集。
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 %}