Django ModelForm的多对多字段

85 投票
9 回答
86242 浏览
提问于 2025-04-15 18:59

考虑以下模型和表单:

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

当你查看这个配料表单时,你可以选择配料放在什么披萨上,一切都很顺利。

我想问的是:我该如何定义一个披萨的模型表单,这样我就能利用披萨和配料之间的多对多关系,并选择哪些配料放在披萨上呢?

9 个回答

7

老实说,我会把多对多的关系放在Pizza模型里。我觉得这样更符合实际情况。想象一下,有个人点了好几份披萨。他不会说“我想在第一和第二个披萨上加奶酪,在第一和第三个披萨上加西红柿”,而是更可能说“一个披萨加奶酪,一个披萨加奶酪和西红柿,…”。

当然,你可以按照自己的方式让表单工作,但我会选择:

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

我不太确定我完全理解这个问题,所以我会基于这个假设来解释:

每个 Pizza(比萨饼)可以有很多 Topping(配料)。而每个 Topping 也可以对应很多 Pizza。但是,如果一个 Topping 被添加到一个 Pizza 上,那么这个 Topping 就会自动和这个 Pizza 关联起来,反之亦然。

在这种情况下,最好的办法是使用一个关系表,这在 Django 中支持得很好。这个关系表可能看起来像这样:

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

示例:

>>> 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>'
139

我想你需要在你的 PizzaForm 中添加一个新的 ModelMultipleChoiceField,然后手动把这个表单字段和模型字段连接起来,因为 Django 不会自动帮你做这件事。

下面的代码片段可能会对你有帮助:

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

这个 PizzaForm 之后可以在任何地方使用,甚至在后台管理系统中:

# 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)

注意

save() 方法可能有点啰嗦,但如果你不需要处理 commit=False 的情况,可以把它简化成这样:

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

撰写回答