Django Forms for Many-to-Many Fields

Alice Ridgway
The Startup
Published in
4 min readSep 26, 2020

--

Building forms with Django is straightforward if your fields are simple inputs like text fields, but what if your forms need to reference other models?

In this tutorial, we will build a form with a group of checkboxes, where the options come from another model.

This post just focuses on rendering forms. If you’re looking for a full worked-example, then check out my post on adding tags to blog posts, which also uses a ManyToManyField.

Building forms with ManyToMany

I’m currently building a meal-planning app. To give users flexibility, they will have the option to create separate plans for different household members.

These are my models. I have a class called ‘Meal’, which references a class called ‘Member’.

meals/models.pyclass Meal(models.Model):    name = models.CharField(max_length=255)
date = models.DateField()
members = models.ForeignKey(Member, on_delete=models.PROTECT)
household/models.pyclass Member(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(max_length=100)
photo = models.ImageField(
upload_to=’uploads/members/’, default=’/icons/member.svg’)
def __str__(self):
return ‘%s Household | %s’ % (self.user, self.name)

I want users to select which household members should be included for a meal.

I could use a foreign key field, but that will only let me choose one member.

The ForeignKey field doesn’t work here because we can only select one member per meal.

Using a ForeignKey to link Meal with Member doesn’t work because I can only select one member per meal.

We need to use a ManyToManyField instead.

ManyToMany

A ManyToMany field is used when a model needs to reference multiple instances of another model. Use cases include:

  • A user needs to assign multiple categories to a blog post
  • A user wants to add multiple blog posts to a publication

In this case, the user needs to be able to add multiple members to a meal. If the choices were fixed and the same for every user, then we could use django-multiple-select-field. If the choices are customizable (i.e. you can do CRUD on the choices), then the choices will have its own model and must be linked using a ManyToManyField.

The model will now look like this:

models.pyclass Meal(models.Model):    name = models.CharField(max_length=255)    date = models.DateField()    members = models.ManyToManyField(Member)
The ManyToManyField provides the ability to select multiple members, but the default form widget is bad for user experience.

This is okay. The core functionality is there but the form needs a lot of work.

For one, I need to cmd-click to select multiple users. This is bad for user experience; checkboxes would be much more user-friendly.

The labels have been taken straight from admin. I want the user to see just the first name, not the user’s email as well.

We need a custom form

In Django, we can use class-based views to generate forms without defining one in forms.py. This is often fine, but Django chooses the form widgets for us. Sometimes, we need a bit more control.

We can create a custom form class in forms.py where we can choose which of Django’s form widgets get used for each field.

Here, we set the members field to use the CheckboxSelectMultiple widget instead of the default multiple choice field.

ManyToManyField is a subclass of django.models but not of django.forms. Instead, we use ModelMultipleChoiceField when referring to forms.

forms.pyclass CreateMealForm(forms.ModelForm):    class Meta:
model = Meal
fields = [‘name’, ‘date’, ‘members’]
name = forms.CharField()
date = forms.DateInput()
members = forms.ModelMultipleChoiceField(
queryset=Member.objects.all(),
widget=forms.CheckboxSelectMultiple
)

ModelMultipleChoiceField takes an argument called ‘queryset’. This lets us control which instances of the Member class will be displayed as options.

If you didn’t already have a custom form, you will need to edit your view to include your new formclass.

views.pyclass AddMeal(CreateView):
model = Meal
form_class = CreateMealForm
template_name = ‘meals/add_meal.html’
success_url = reverse_lazy(‘index’)

We’ve got our checkboxes but we still have a couple of issues.

We can tweak our CSS to remove the bullet points and execute a more polished design. But first, we need to change the labels for the checkboxes.

Custom Labels

To control the checkbox labels, we must tweak the ModelMultipleChoiceField.

To do this, we:

  • Create a custom form class which inherits from ModelMultipleChoiceField
  • Override the ‘label_from_instance’ method
  • Replace the reference to forms.ModelMultipleChoiceField to our new custom class.

Our forms.py will now look like this:

forms.pyfrom django import forms
from .models import Meal
from core.models import Member
class CustomMMCF(forms.ModelMultipleChoiceField): def label_from_instance(self, member):
return “%s” % member.name
class CreateMealForm(forms.ModelForm): class Meta:
model = Meal
fields = [‘name’, ‘date’, ‘members’]
name = forms.CharField()
date = forms.DateInput()
members = CustomMMCF(
queryset=Member.objects.all(),
widget=forms.CheckboxSelectMultiple
)

At this point, the back-end work is **almost** complete.

Wait, there’s a bug?

See queryset=Member.objects.all()?

forms.pymembers = CustomMMCF(
queryset=Member.objects.all(),
widget=forms.CheckboxSelectMultiple
)

This will return members for every user. The question is, how do you pass the request.user object to a form class?

This bug deserves its own post. It’s right here: 👇

Thanks for reading.

--

--