Django ModelForm für viele-zu-viele Felder

79

Betrachten Sie die folgenden Modelle und Formulare:

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

Wenn Sie sich die ToppingForm ansehen, können Sie auswählen, auf welchen Pizzen die Toppings liegen, und alles ist nur Dandy.

Meine Fragen lauten: Wie definiere ich ein ModelForm für Pizza, mit dem ich die Viele-zu-Viele-Beziehung zwischen Pizza und Topping nutzen und auswählen kann, welche Toppings auf die Pizza passen?

sie rufen mich an
quelle
Also aus Ihren Kommentaren unten: Jeder Pizzakann viele Toppings haben. Jeder Toppingkann viele Pizzas haben. Aber wenn ich a Toppingzu a hinzufüge Pizza, hat das Pizzadann automatisch ein a Toppingund umgekehrt?
Jack M.

Antworten:

132

Ich denke , man würde hier eine neue hinzufügen , ModelMultipleChoiceFieldum Ihre PizzaFormund verknüpfen manuell dieses Formularfeld mit dem Modellfeld, wie Django wird das nicht automatisch für Sie.

Das folgende Snippet könnte hilfreich sein:

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

Dies PizzaFormkann dann überall verwendet werden, auch im 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)

Hinweis

Die save()Methode ist vielleicht etwas zu ausführlich, aber Sie können sie vereinfachen, wenn Sie die commit=FalseSituation nicht unterstützen müssen. Dann sieht es so aus :

def save(self):
  instance = forms.ModelForm.save(self)
  instance.topping_set.clear()
  instance.topping_set.add(*self.cleaned_data['toppings'])
  return instance
Clément
quelle
Das sieht cool aus, aber ich verstehe den Code nicht ganz, besonders die 'Instanz', save_m2m und old_save_m2m :)
Viet
1
@Viet: In der Dokumentation zu Formularen von django ( docs.djangoproject.com/de/dev/topics/forms/modelforms/… ) können Sie sehen, dass django Ihrem automatisch eine save_m2mMethode hinzufügt , ModelFormwenn Sie es aufrufen save(commit=False). Genau das mache ich hier, indem ich eine save_m2mMethode zum Speichern verwandter Objekte und Beläge hinzufüge. Diese Methode ruft das Original auf save_m2m.
Clément
3
Wie ist diese Lösung besser als die von Jack M., dh die Einführung eines Zwischenmodells? Diese Lösung scheint viel mehr Code zu erfordern.
mb21
Könnte diese Logik für jedes umgekehrte M2M wiederverwendbar sein, beispielsweise mit einem Mixin, einem Dekorateur oder etwas anderem?
David D.
16

Ich bin mir nicht sicher, ob ich die Frage zu 100% bekomme, also gehe ich von dieser Annahme aus:

Jeder Pizzakann viele Toppings haben. Jeder Toppingkann viele Pizzas haben. Aber wenn a Toppingzu a hinzugefügt wird, Pizzahat das Toppingautomatisch a Pizzaund umgekehrt.

In diesem Fall ist eine Beziehungstabelle die beste Wahl, die Django recht gut unterstützt. Es könnte so aussehen:

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

Beispiel:

>>> 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>'
Jack M.
quelle
AttributeError at / require / add / Kann keine Werte für ein ManyToManyField festlegen, das ein Zwischenmodell angibt. Verwenden Sie stattdessen Anforderungen. Manager von AssetRequirement.
Eloy Roldán Paredes
7

Um ehrlich zu sein, würde ich die Viele-zu-Viele-Beziehung in das PizzaModell einbauen. Ich denke das näher an der Realität. Stellen Sie sich eine Person vor, die mehrere Pizzen bestellt. Er würde nicht sagen "Ich möchte Käse auf Pizza eins und zwei und Tomaten auf Pizza eins und drei", sondern wahrscheinlich "Eine Pizza mit Käse, eine Pizza mit Käse und Tomaten, ...".

Natürlich ist es möglich, dass das Formular auf Ihre Weise funktioniert, aber ich würde folgendermaßen vorgehen:

class Pizza(models.Model):
    name = models.CharField(max_length=50)
    toppings = models.ManyToManyField(Topping)
Felix Kling
quelle
5
Die Pizza / Topping-Modelle sind nur eine Verkleidung für meine echten Modelle. Der Zweck dieser Frage ist, dass ich möchte, dass ich mit der Pizza ModelForm Toppings und mit der Topping ModelForm Pizzas auswählen kann.
rufen
4

Eine andere einfache Möglichkeit, dies zu erreichen, besteht darin, eine Zwischentabelle zu erstellen und Inline-Felder zu verwenden, um dies zu erreichen. Weitere Informationen finden Sie unter https://docs.djangoproject.com/de/1.2/ref/contrib/admin/#working-with-many-to-many-intermediary-models

Einige Beispielcodes unten

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)
Hoang HUA
quelle
Ich gehe davon aus, dass dies nur auf Admin-Seiten / Formularen wirksam wird. Wie würden Sie ähnliche für anonyme / Gastbenutzer und / oder angemeldete Benutzer erstellen, um beispielsweise eine Pizza-Präferenz veröffentlichen zu können?
user1271930
2

Ich bin mir nicht sicher, ob Sie danach suchen, aber wissen Sie, dass Pizza das topping_setAttribut hat? Mit diesem Attribut können Sie Ihrem ModelForm problemlos einen neuen Belag hinzufügen.

new_pizza.topping_set.add(new_topping)
Buckley
quelle
2

Wir hatten ein ähnliches Problem in unserer App, die django admin verwendete. Es gibt viele zu viele Beziehungen zwischen Benutzern und Gruppen, und man kann Benutzer nicht einfach zu einer Gruppe hinzufügen. Ich habe einen Patch für Django erstellt, der dies tut, aber es wird nicht viel darauf geachtet ;-) Sie können ihn lesen und versuchen, eine ähnliche Lösung für Ihr Pizza- / Topping-Problem anzuwenden. Auf diese Weise können Sie in einem Belag ganz einfach verwandte Pizzen hinzufügen oder umgekehrt.

gruszczy
quelle
0

Ich habe etwas Ähnliches basierend auf dem Code von Clément mit einem Benutzerverwaltungsformular gemacht:

# 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)
user324541
quelle
0

Sie können auch eine Durch-Tabelle verwenden, wenn Sie Inhalte hinzufügen möchten, die von beiden Primärschlüsseln der Tabelle in der Beziehung abhängig sind. Viele bis viele Beziehungen verwenden eine sogenannte Brückentabelle, um Dinge zu speichern, die von beiden Teilen des Primärschlüssels abhängig sind.

Betrachten Sie beispielsweise die folgende Beziehung zwischen Bestellung und Produkt in models.py

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

In Ihrem Fall würden Sie das ManyToMany auf die Beläge legen, da der Benutzer damit auswählen kann, welche Beläge auf die gewünschte Pizza passen. Einfache, aber leistungsstarke Lösung.

Alfred Tsang
quelle