Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django ModelForm for Many-to-Many fields

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?

like image 367
theycallmemorty Avatar asked Feb 07 '10 13:02

theycallmemorty


People also ask

How take data from many to many field in Django?

A ManyToManyField in Django is a field that allows multiple objects to be stored. This is useful and applicable for things such as shopping carts, where a user can buy multiple products. To add an item to a ManyToManyField, we can use the add() function.

How does Django many to many field work?

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.

How do you update many to many fields?

You need to use . set() or . add() for updating a M2M field.


2 Answers

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 
like image 63
Clément Avatar answered Sep 20 '22 20:09

Clément


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>' 
like image 35
Jack M. Avatar answered Sep 20 '22 20:09

Jack M.