Django ModelForm dla pól wiele do wielu

80

Rozważ następujące modele i 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

Kiedy przeglądasz ToppingForm, pozwala wybrać, jakie pizze mają być dodawane i wszystko jest po prostu eleganckie.

Moje pytania to: Jak zdefiniować Formularz Modelowy dla pizzy, który pozwoli mi skorzystać z relacji „wiele do wielu” między pizzą i polewą, a także pozwoli mi wybrać, jakie dodatki znajdą się w pizzy?

theycallmemorty
źródło
A więc z twoich komentarzy poniżej: Każdy Pizzamoże mieć wiele Toppings. Każdy Toppingmoże mieć wiele Pizzas. Ale jeśli dodam a Toppingdo a Pizza, czy to Pizzawtedy automagicznie ma a Toppingi odwrotnie?
Jack M.

Odpowiedzi:

132

Myślę, że musiałbyś tutaj dodać nowe ModelMultipleChoiceFielddo swojego PizzaFormi ręcznie połączyć to pole formularza z polem modelu, ponieważ Django nie zrobi tego automatycznie za Ciebie.

Pomocny może być następujący fragment kodu:

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

To PizzaFormmoże być następnie wykorzystane wszędzie, nawet w 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)

Uwaga

save()Metoda może być nieco zbyt rozwlekły, ale można uprościć, jeśli nie trzeba wspierać commit=Falsesytuację, to wtedy tak:

def save(self):
  instance = forms.ModelForm.save(self)
  instance.topping_set.clear()
  instance.topping_set.add(*self.cleaned_data['toppings'])
  return instance
Łaskawy
źródło
Wygląda fajnie, ale nie do końca rozumiem kod, szczególnie „instancja”, save_m2m i old_save_m2m :)
Viet
1
@Viet: w dokumentacji django na temat formularzy ( docs.djangoproject.com/en/dev/topics/forms/modelforms/... ) możesz zobaczyć, że django automatycznie dodaje save_m2mmetodę do twojej, ModelFormgdy ją wywołujesz save(commit=False). To jest dokładnie to, co tutaj robię, dodając save_m2mmetodę zapisywania powiązanych obiektów i dodatków , a ta metoda wywołuje oryginał save_m2m.
Clément
3
W czym to rozwiązanie jest lepsze od Jacka M., czyli wprowadzenia modelu pośredniego? Wydaje się, że to rozwiązanie wymaga znacznie więcej kodu.
mb21
Czy ta logika może być wielokrotnego użytku dla dowolnego odwróconego M2M, używając na przykład miksera, dekoratora lub czegoś innego?
David D.
16

Nie jestem pewien, czy otrzymam pytanie w 100%, więc zamierzam działać z tym założeniem:

Każdy Pizzamoże mieć wiele Toppings. Każdy Toppingmoże mieć wiele Pizzas. Ale jeśli a Toppingzostanie dodane do a Pizza, Toppingto automagicznie będzie mieć a Pizzai na odwrót.

W tym przypadku najlepszym rozwiązaniem jest tabela relacji, którą Django całkiem dobrze obsługuje. Mogłoby to wyglądać tak:

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

Przykład:

>>> 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.
źródło
AttributeError at / require / add / Cannot set values ​​on a ManyToManyField, który określa model pośredni. Zamiast tego użyj Requirements.AssetRequirement's Manager.
Eloy Roldán Paredes,
7

Szczerze mówiąc, umieściłbym w Pizzamodelu relację wiele do wielu . Myślę, że jest to bliższe rzeczywistości. Wyobraź sobie osobę, która zamawia kilka pizz. Nie powiedziałby „Chciałbym sera na pierwszej i drugiej pizzy i pomidorów na pierwszej i trzeciej”, ale prawdopodobnie „Jedna pizza z serem, jedna pizza z serem i pomidorami…”.

Oczywiście możliwe jest, aby formularz działał na Twój sposób, ale ja bym poszedł z:

class Pizza(models.Model):
    name = models.CharField(max_length=50)
    toppings = models.ManyToManyField(Topping)
Felix Kling
źródło
5
Modele Pizza / Topping to tylko przebranie moich prawdziwych modelek. Celem tego pytania jest to, że chcę, aby Pizza ModelForm pozwoliła mi wybrać Toppings i chcę, aby Topping ModelForm pozwolił mi wybrać Pizzę.
theycallmemorty
4

Innym prostym sposobem na osiągnięcie tego jest utworzenie tabeli pośredniej i użycie pól wbudowanych, aby to zrobić. Proszę odnieść się do https://docs.djangoproject.com/en/1.2/ref/contrib/admin/#working-with-many-to-many-intermediary-models

Poniżej przykładowy kod

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
źródło
Zakładam, że działa to tylko na stronach / formularzach administratora. W jaki sposób stworzyłbyś coś podobnego dla anonimowych / gości i / lub zalogowanych użytkowników, aby mogli na przykład publikować preferencje dotyczące pizzy?
user1271930
2

Nie jestem pewien, czy tego właśnie szukasz, ale czy wiesz, że Pizza ma ten topping_setatrybut? Używając tego atrybutu, możesz łatwo dodać nowy dodatek w swoim ModelForm.

new_pizza.topping_set.add(new_topping)
Buckley
źródło
2

Podobny problem mieliśmy w naszej aplikacji korzystającej z administratora django. Istnieje wiele do wielu relacji między użytkownikami i grupami i nie można łatwo dodawać użytkowników do grupy. Stworzyłem łatkę dla django, która to robi, ale nie ma na to zbyt wiele uwagi ;-) Możesz ją przeczytać i spróbować zastosować podobne rozwiązanie do problemu z pizzą / polewą. W ten sposób będąc w polewie, możesz łatwo dodać powiązane pizze lub odwrotnie.

gruszczy
źródło
0

Zrobiłem coś podobnego w kodzie Clémenta z formularzem administratora użytkownika:

# 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
źródło
0

Możesz również użyć tabeli przelotowej, jeśli chcesz dodać elementy zależne od obu kluczy podstawowych tabeli w relacji. Wiele do wielu relacji używa czegoś, co nazywa się tablicą mostkową do przechowywania rzeczy zależnych od obu części klucza podstawowego.

Weźmy na przykład pod uwagę następującą relację między zamówieniem a produktem w 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()

W twoim przypadku to, co byś zrobił, to umieszczenie ManyToMany na polewach, ponieważ pozwala on użytkownikowi wybrać, jakie dodatki do pizzy chciał. Proste, ale potężne rozwiązanie.

Alfred Tsang
źródło