Django ModelForm for Many-to-Many fields

PythonDjangoDjango Forms

Python Problem Overview


Consider the following models and form:

class Pizza(models.Model):
    name = models.CharField(max_length=50)

class Topping(models.Model):
    name = models.CharField(max_length=50)
    ison = models.ManyToManyField(Pizza, blank=True)

class ToppingForm(forms.ModelForm):
    class Meta:
        model = Topping

When you view the ToppingForm it lets you choose what pizzas the toppings go on and everything is just dandy.

My questions is: How do I define a ModelForm for Pizza that lets me take advantage of the Many-to-Many relationship between Pizza and Topping and lets me choose what Toppings go on the Pizza?

Python Solutions


Solution 1 - Python

I guess you would have here to add a new ModelMultipleChoiceField to your PizzaForm, and manually link that form field with the model field, as Django won't do that automatically for you.

The following snippet might be helpful :

class PizzaForm(forms.ModelForm):
    class Meta:
        model = Pizza

    # Representing the many to many related field in Pizza
    toppings = forms.ModelMultipleChoiceField(queryset=Topping.objects.all())

    # Overriding __init__ here allows us to provide initial
    # data for 'toppings' field
    def __init__(self, *args, **kwargs):
        # Only in case we build the form from an instance
        # (otherwise, 'toppings' list should be empty)
        if kwargs.get('instance'):
            # We get the 'initial' keyword argument or initialize it
            # as a dict if it didn't exist.                
            initial = kwargs.setdefault('initial', {})
            # The widget for a ModelMultipleChoiceField expects
            # a list of primary key for the selected data.
            initial['toppings'] = [t.pk for t in kwargs['instance'].topping_set.all()]

        forms.ModelForm.__init__(self, *args, **kwargs)

    # Overriding save allows us to process the value of 'toppings' field    
    def save(self, commit=True):
        # Get the unsave Pizza instance
        instance = forms.ModelForm.save(self, False)

        # Prepare a 'save_m2m' method for the form,
        old_save_m2m = self.save_m2m
        def save_m2m():
           old_save_m2m()
           # This is where we actually link the pizza with toppings
           instance.topping_set.clear()
           instance.topping_set.add(*self.cleaned_data['toppings'])
        self.save_m2m = save_m2m

        # Do we need to save all changes now?
        if commit:
            instance.save()
            self.save_m2m()

        return instance

This PizzaForm can then be used everywhere, even in the admin :

# yourapp/admin.py
from django.contrib.admin import site, ModelAdmin
from yourapp.models import Pizza
from yourapp.forms import PizzaForm

class PizzaAdmin(ModelAdmin):
  form = PizzaForm

site.register(Pizza, PizzaAdmin)
Note

The save() method might be a bit too verbose, but you can simplify it if you don't need to support the commit=False situation, it will then be like that :

def save(self):
    instance = forms.ModelForm.save(self)
    instance.topping_set.clear()
    instance.topping_set.add(*self.cleaned_data['toppings'])
    return instance

Solution 2 - Python

I'm not certain I get the question 100%, so I'm going to run with this assumption:

Each Pizza can have many Toppings. Each Topping can have many Pizzas. But if a Topping is added to a Pizza, that Topping then automagically will have a Pizza, and vice versa.

In this case, your best bet is a relationship table, which Django supports quite well. It could look like this:

models.py

class PizzaTopping(models.Model):
    topping = models.ForeignKey('Topping')
    pizza = models.ForeignKey('Pizza')
class Pizza(models.Model):     
    name = models.CharField(max_length=50) 
    topped_by = models.ManyToManyField('Topping', through=PizzaTopping)
    def __str__(self):
        return self.name
    def __unicode__(self):
        return self.name
class Topping(models.Model):   
    name=models.CharField(max_length=50)
    is_on = models.ManyToManyField('Pizza', through=PizzaTopping)
    def __str__(self):
        return self.name
    def __unicode__(self):
        return self.name

forms.py

class PizzaForm(forms.ModelForm):
    class Meta:
        model = Pizza
class ToppingForm(forms.ModelForm):
    class Meta:
        model = Topping

Example:

>>> p1 = Pizza(name="Monday")
>>> p1.save()
>>> p2 = Pizza(name="Tuesday")
>>> p2.save()
>>> t1 = Topping(name="Pepperoni")
>>> t1.save()
>>> t2 = Topping(name="Bacon")
>>> t2.save()
>>> PizzaTopping(pizza=p1, topping=t1).save() # Monday + Pepperoni
>>> PizzaTopping(pizza=p2, topping=t1).save() # Tuesday + Pepperoni
>>> PizzaTopping(pizza=p2, topping=t2).save() # Tuesday + Bacon

>>> tform = ToppingForm(instance=t2) # Bacon
>>> tform.as_table() # Should be on only Tuesday.
u'<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" value="Bacon" maxlength="50" /></td></tr>\n<tr><th><label for="id_is_on">Is on:</label></th><td><select multiple="multiple" name="is_on" id="id_is_on">\n<option value="1">Monday</option>\n<option value="2" selected="selected">Tuesday</option>\n</select><br /> Hold down "Control", or "Command" on a Mac, to select more than one.</td></tr>'

>>> pform = PizzaForm(instance=p1) # Monday
>>> pform.as_table() # Should have only Pepperoni
u'<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" value="Monday" maxlength="50" /></td></tr>\n<tr><th><label for="id_topped_by">Topped by:</label></th><td><select multiple="multiple" name="topped_by" id="id_topped_by">\n<option value="1" selected="selected">Pepperoni</option>\n<option value="2">Bacon</option>\n</select><br /> Hold down "Control", or "Command" on a Mac, to select more than one.</td></tr>'

>>> pform2 = PizzaForm(instance=p2) # Tuesday
>>> pform2.as_table() # Both Pepperoni and Bacon
u'<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" value="Tuesday" maxlength="50" /></td></tr>\n<tr><th><label for="id_topped_by">Topped by:</label></th><td><select multiple="multiple" name="topped_by" id="id_topped_by">\n<option value="1" selected="selected">Pepperoni</option>\n<option value="2" selected="selected">Bacon</option>\n</select><br /> Hold down "Control", or "Command" on a Mac, to select more than one.</td></tr>'

Solution 3 - Python

To be honest, I would put the many-to-many relation into the Pizza model. I think this closer to reality. Imagine a person that orders several pizzas. He wouldn't say "I would like cheese on pizza one and two and tomatoes on pizza one and three" but probably "One pizza with cheese, one pizza with cheese and tomatoes,...".

Of course it is possible to get the form working in your way but I would go with:

class Pizza(models.Model):
    name = models.CharField(max_length=50)
    toppings = models.ManyToManyField(Topping)

Solution 4 - Python

Another simple way to achieve this is to create an intermediary table and using inline fields to get it done. Please refer to this https://docs.djangoproject.com/en/1.2/ref/contrib/admin/#working-with-many-to-many-intermediary-models

Some sample code below

models.py

class Pizza(models.Model):
    name = models.CharField(max_length=50)

class Topping(models.Model):
    name = models.CharField(max_length=50)
    ison = models.ManyToManyField(Pizza, through='PizzaTopping')

class PizzaTopping(models.Model):
    pizza = models.ForeignKey(Pizza)
    topping = models.ForeignKey(Topping)

admin.py

class PizzaToppingInline(admin.TabularInline):
    model = PizzaTopping

class PizzaAdmin(admin.ModelAdmin):
    inlines = [PizzaToppingInline,]

class ToppingAdmin(admin.ModelAdmin):
    inlines = [PizzaToppingInline,]

admin.site.register(Pizza, PizzaAdmin)
admin.site.register(Topping, ToppingAdmin)

Solution 5 - Python

I'm not sure if this is what your looking for, but are you aware that Pizza has the topping_set attribute? Using that attribute you could easily add a new topping in your ModelForm.

new_pizza.topping_set.add(new_topping)

Solution 6 - Python

We had similar problem in our app, which used django admin. There is many to many relation between users and groups and one can't easily add users to a group. I have created a patch for django, that does this, but there isn't much attention to it ;-) You can read it and try to apply similar solution to your pizza/topping problem. This way being inside a topping, you can easily add related pizzas or vice versa.

Solution 7 - Python

I did something similar based in code of Clément with a user admin form:

# models.py
class Clinica(models.Model):
  ...
  users = models.ManyToManyField(User, null=True, blank=True, related_name='clinicas')

# admin.py
class CustomUserChangeForm(UserChangeForm):
  clinicas = forms.ModelMultipleChoiceField(queryset=Clinica.objects.all())
  
  def __init__(self,*args,**kwargs):
    if 'instance' in kwargs:
      initial = kwargs.setdefault('initial',{})
      initial['clinicas'] = kwargs['instance'].clinicas.values_list('pk',flat=True)
    super(CustomUserChangeForm,self).__init__(*args,**kwargs)

  def save(self,*args,**kwargs):
    instance = super(CustomUserChangeForm,self).save(*args,**kwargs)
    instance.clinicas = self.cleaned_data['clinicas']
    return instance

  class Meta:
    model = User

admin.site.unregister(User)

UserAdmin.fieldsets += ( (u'Clinicas', {'fields': ('clinicas',)}), )
UserAdmin.form = CustomUserChangeForm

admin.site.register(User,UserAdmin)

Solution 8 - Python

You can also use a through table if you want to add stuff that is dependent on both primary keys of the table in the relationship. Many to many relationships use something called a bridge table to store stuff that is dependent on both parts of the primary key.

For example, consider the following relationship between Order and Product in models.py

class Order(models.Model):
    date = models.DateField()
    status = models.CharField(max_length=30)

class Product(models.Model):
    name = models.CharField(max_length=50)
    desc = models.CharField(max_length=50)
    price = models.DecimalField(max_dights=7,decimal_places=2)
    qtyOnHand = models.Integer()
    orderLine = models.ManyToManyField(Order, through='OrderLine')

class OrderLine(models.Model):
    product = models.ForeignKey(Product)
    order = models.ForeignKey(Order)
    qtyOrd = models.Integer()

In your case, what you would do is put the ManyToMany on the Toppings, because it lets the user choose what toppings go on the Pizza they wanted. Simple, but powerful solution.

Solution 9 - Python

This looks like a job for Django's BaseInlineFormset.

You can create one easily using the inlineformset_factory.

It is even possible to use the ManyToManyField's implicit through table, so you do not need to create an explicit PizzaTopping table yourself.

Here's a slightly modified version of the OP's example:

class Pizza(models.Model):
    name = models.CharField(max_length=50)
    toppings = models.ManyToManyField(Topping, blank=True)

class Topping(models.Model):
    name = models.CharField(max_length=50)

and here's how to create, and use, an inline formset for these models, e.g. in a view

...
PizzaToppingFormSet = inlineformset_factory(
    parent_model=Pizza, model=Pizza.toppings.through, fields='__all__')
...
pizza = Pizza.objects.get(...)  # however you get your pizza object
...
formset = PizzaToppingFormset(instance=pizza, data=request.POST)
...

For details on using these formsets in a view, check the documentation.

Note that the value of formset.prefix when using the through table would be Pizza_toppings, so your POST data would look like this, for example:

data = {
    ...
    'Pizza_toppings-TOTAL_FORMS': '2',
    'Pizza_toppings-INITIAL_FORMS': '0',
    'Pizza_toppings-0-name': 'pepperoni',
    'Pizza_toppings-1-name': 'cheese',
    ...
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestiontheycallmemortyView Question on Stackoverflow
Solution 1 - PythonClémentView Answer on Stackoverflow
Solution 2 - PythonJack M.View Answer on Stackoverflow
Solution 3 - PythonFelix KlingView Answer on Stackoverflow
Solution 4 - PythonHoang HUAView Answer on Stackoverflow
Solution 5 - PythonbuckleyView Answer on Stackoverflow
Solution 6 - PythongruszczyView Answer on Stackoverflow
Solution 7 - Pythonuser324541View Answer on Stackoverflow
Solution 8 - PythonAlfred TsangView Answer on Stackoverflow
Solution 9 - PythondjvgView Answer on Stackoverflow