Django ModelForm用于多对多字段


79

考虑以下模型和形式:

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

当您查看ToppingForm时,它使您可以选择浇头是什么比萨饼,而一切都花哨的东西。

我的问题是:如何为披萨定义一个ModelForm,让我利用披萨和馅料之间的多对多关系,并让我选择披萨上的馅料?


因此,从下面的注释中Pizza可以看出:每个可以有多个Toppings。每个Topping可以有多个Pizzas。但是,如果我添加一个ToppingPizza,这是否Pizza然后自动地有一个Topping,反之亦然?
Jack M. 2010年

Answers:


132

我想你会在这里新添加ModelMultipleChoiceField到您PizzaForm,并手动链接,表单字段与模型领域,如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然后可以使用无处不在,甚至在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)

注意

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

这看起来很酷,但是我不太理解代码,特别是'instance',save_m2m和old_save_m2m :)
越南,

1
@Viet:在django表单文档(docs.djangoproject.com/en/dev/topics/forms/modelforms/…)中,您可以看到django在您调用它时会自动向其中添加一个save_m2m方法。这正是我在这里所做的,添加了一种保存相关对象和浇头的方法,该方法称为original 。ModelFormsave(commit=False)save_m2msave_m2m
克莱门特

2
该解决方案比Jack M.的解决方案(即引入中间模型)如何更好?该解决方案似乎需要更多代码。
mb21 2013年

对于任何反向M2M,例如使用mixin,装饰器或其他东西,这种逻辑是否可以重用?
David D.

16

我不确定我是否会100%回答这个问题,因此我将按照以下假设运行:

每个Pizza可以有多个Toppings。每个Topping可以有多个Pizzas。但是,如果将aTopping添加到PizzaTopping则将自动具有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

表格

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

/ requirements / add /处的AttributeError无法在用于指定中介模型的ManyToManyField上设置值。请使用requirements.AssetRequirement的Manager。
埃洛伊·罗丹(EloyRoldánParedes)

7

老实说,我将多对多关系放入Pizza模型中。我认为这更接近现实。想象一个人要订购几个比萨饼。他不会说“我想要一和两个披萨上的奶酪,而一和三个披萨上的番茄”,但可能是“一个芝士披萨,一个芝士和番茄披萨,...”。

当然可以使表格以您的方式工作,但我会同意:

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

5
Pizza / Topping模型只是我的真实模型的伪装。这个问题的目的是因为我希望Pizza ModelForm让我选择浇头,并且希望Topping ModelForm让我选择披萨。
callmemorty 2010年

3

实现此目的的另一种简单方法是创建一个中间表,并使用内联字段将其完成。请参阅此https://docs.djangoproject.com/en/1.2/ref/contrib/admin/#working-with-many-to-many-intermediary-models

下面的一些示例代码

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)

管理员

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)

我认为这仅对管理页面/表单有效。您将如何为匿名/来宾用户和/或已登录用户创建类似内容以发布例如披萨偏好设置?
user1271930

2

我不确定这是否是您要寻找的东西,但是您知道Pizza具有该topping_set属性吗?使用该属性,您可以轻松地在ModelForm中添加新的顶部。

new_pizza.topping_set.add(new_topping)

2

我们在使用django admin的应用程序中遇到了类似的问题。用户和组之间存在许多关系,而且不能轻易将用户添加到组中。我已经为django创建了一个补丁,可以做到这一点,但是对此并没有太多关注;-)您可以阅读并尝试将类似的解决方案应用于您的披萨/打顶问题。这样,您就可以轻松地添加相关的比萨饼,反之亦然。


0

我使用用户管理表单基于Clément代码做了类似的事情:

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

0

如果要添加依赖于关系中表的两个主键的内容,也可以使用穿透表。多对多关系使用称为桥接表的东西来存储依赖于主键的两个部分的内容。

例如,考虑models.py中Order和Product之间的以下关系

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

在您的情况下,您将要做的是将ManyToMany放在浇头上,因为它使用户可以选择想要的披萨上的浇头。简单但功能强大的解决方案。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.