I’m making some progress in solving this. What I want to model is this:
I have a model for materials, a model for projects, and an intermediary model to specify more data to the many-to-many relationship between them. The objective is to allow a project not only to have multiple materials but also to specify a quantity for each.
These are my models:
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)
Now, I have successfully rendered the project form containing a checkbox list of the materials (without the quantity feature), another form listing just one material as a checkbox, and a field for the quantity (a form of the ProjectMaterialSet model).
Now, the part I’m struggling with is how to combine both. So far, I think what I need to do is have a query set of the available materials in the ProjectForm, to then add to it a series of sub-forms (here is where I think I should use formsets, however, I’m unsure how) that passes each material pk so I can render each form as a ModelMultipleChoiceField so I get to use the checkbox widget for each material.
My natural instinct as someone not related with formsets is to for-loop over the materials and create form instances that save to a dict or something around those lines. However, I read on the Django forum a good phrase about using Django features instead of hacking a solution, and I do feel the formsets can be used for this, I’m just unsure how.
Clarification: I need to pass each material pk to the ProjectMaterialSetForm so I can get the checkbox for each material (as I’m getting a query set with just one result since I’m filtering by pk), and link a quantity input to that single checkbox.
I feel I have most of it done, I’m just not sure how to add the sub-forms, and if someone could also help about the custom save and validation required, that’d be amazing!
Here are my forms:
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)