Django ModelForm для полей "многие ко многим"

80

Рассмотрим следующие модели и формы:

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. Но если я добавлю a Toppingк a Pizza, будет ли это Pizzaавтоматически иметь a Topping, и наоборот?
Джек М.

Ответы:

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может быть использовано везде, даже в админке:

# 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 :)
Viet
1
@Viet: в документации django по формам ( docs.djangoproject.com/en/dev/topics/forms/modelforms/… ) вы можете видеть, что django автоматически добавляет save_m2mметод к вашему, ModelFormкогда вы его вызываете save(commit=False). Это именно то, что я здесь делаю, добавляя save_m2mметод для сохранения связанных объектов и начинок , и этот метод вызывает оригинал save_m2m.
Clément
3
Чем это решение лучше, чем у Джека М., то есть введение промежуточной модели? Это решение, похоже, требует намного больше кода.
mb21,
Можно ли повторно использовать эту логику для любого обратного M2M, используя, например, миксин, декоратор или что-то еще?
Дэвид Д.
16

Я не уверен, что отвечу на вопрос на 100%, поэтому я буду исходить из следующего предположения:

У каждого Pizzaможет быть много Toppings. У каждого Toppingможет быть много Pizzas. Но если Toppingдобавить к a Pizza, Toppingтогда автоматически будет a 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>'
Джек М.
источник
AttributeError в / requirements / add / Невозможно установить значения в ManyToManyField, который указывает промежуточную модель. Вместо этого используйте requirements.AssetRequirement's Manager.
Элой Рольдан Паредес
7

Честно говоря, я бы поместил в Pizzaмодель отношение «многие ко многим» . Думаю, это ближе к реальности. Представьте себе человека, который заказывает несколько пицц. Он не сказал бы: «Я бы хотел сыр на пиццу номер один и второй и помидоры на пиццу один и три», но, вероятно, «Одна пицца с сыром, одна пицца с сыром и помидорами, ...».

Конечно, можно заставить форму работать по-вашему, но я бы пошел с:

class Pizza(models.Model):
    name = models.CharField(max_length=50)
    toppings = models.ManyToManyField(Topping)
Феликс Клинг
источник
5
Модели пиццы / топпинга - это всего лишь маскировка для моих настоящих моделей. Цель этого вопроса заключается в том, что я хочу, чтобы форма модели пиццы позволяла мне выбирать начинки, и я хочу, чтобы форма модели топпинга позволяла мне выбирать пиццу.
theycallmemorty
4

Еще один простой способ добиться этого - создать промежуточную таблицу и использовать для этого встроенные поля. Пожалуйста, обратитесь к этому 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)
Hoang HUA
источник
Я предполагаю, что это действует только на страницах / формах администратора. Как бы вы создали подобное для анонимных / гостевых пользователей и / или вошедших в систему пользователей, чтобы иметь возможность публиковать, например, предпочтение пиццы?
user1271930 09
2

Я не уверен, что это то, что вы ищете, но знаете ли вы, что у пиццы есть topping_setатрибут? Используя этот атрибут, вы можете легко добавить новую вершину в свой ModelForm.

new_pizza.topping_set.add(new_topping)
Бакли
источник
2

У нас была аналогичная проблема в нашем приложении, которое использовало админку django. Между пользователями и группами существует связь "многие ко многим", и добавить пользователей в группу нелегко. Я создал патч для django, который делает это, но не уделяет ему особого внимания ;-) Вы можете прочитать его и попробовать применить подобное решение к вашей проблеме с пиццей / топпингом. Таким образом, находясь внутри топпинга, вы можете легко добавить похожие пиццы или наоборот.

Gruszczy
источник
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)
user324541
источник
0

Вы также можете использовать сквозную таблицу, если хотите добавить материал, который зависит от обоих первичных ключей таблицы в отношении. Отношения «многие ко многим» используют так называемую таблицу мостов для хранения данных, зависящих от обеих частей первичного ключа.

Например, рассмотрите следующую связь между заказом и продуктом в 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()

В вашем случае вы бы поместили ManyToMany на начинки, потому что это позволяет пользователю выбирать, какие начинки использовать для пиццы, которую он хочет. Простое, но мощное решение.

Альфред Цанг
источник