Django ModelForm用于多对多字段

考虑以下模型和表单:

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时,它可以让你select什么比萨饼浇头继续,一切都只是丹迪。

我的问题是:如何定义比萨的ModelForm模型,使我能够利用Pizza和Topping之间的多对多关系,并让我selectToppings在比萨上的select?

我想你会在这里添加一个新的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() for topping in self.cleaned_data['toppings']: instance.topping_set.add(topping) self.save_m2m = save_m2m # Do we need to save all changes now? if commit: instance.save() self.save_m2m() return instance 

这个PizzaForm可以随处使用,即使在pipe理员:

 # 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() for topping in self.cleaned_data['toppings']: instance.topping_set.add(topping) 

我不确定我是否会得到100%的问题,所以我要运行这个假设:

每个Pizza可以有许多Topping 。 每个Topping可以有很多Pizza 。 但是,如果Topping被添加到Pizza ,那么Topping将自动地有Pizza ,反之亦然。

在这种情况下,你最好的select是一个关系表,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>' 

说实话,我会把多对多的关系放进Pizza模型中。 我觉得这更接近现实。 想象一下,一个人点了几个比萨饼。 他不会说“我想吃比萨饼上的芝士一,二,比萨饼一,三”,但可能是“一个比萨奶酪,一个比萨奶酪和西红柿,…”。

当然有可能让你的表单在你的方式工作,但我会去:

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

我不知道这是你想要的,但是你知道Pizza有topping_set属性吗? 使用该属性,您可以轻松地在您的ModelForm中添加一个新的顶点。

 new_pizza.topping_set.add(new_topping) 

在我们的应用程序中,我们遇到过类似的问题,它使用了django admin。 用户和组之间有很多关系,不能轻易将用户添加到组中。 我已经为django创build了一个补丁程序 ,但是没有太多关注;你可以阅读它,并尝试将类似的解决scheme应用到比萨/打顶问题上。 这种方式是在一个顶部,你可以很容易地添加相关的比萨饼,反之亦然。

另一个简单的方法是创build一个中间表并使用内联字段来完成。 请参阅此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) 

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) 

我使用用户pipe理员的forms在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)