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?
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.
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.
You need to use . set() or . add() for updating a M2M field.
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)
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
I'm not certain I get the question 100%, so I'm going to run with this assumption:
Each Pizza
can have many Topping
s. Each Topping
can have many Pizza
s. 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>'
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With