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

Коли ви переглядаєте форму ToppingFor, вона дозволяє вам вибрати, на якій піці подаватиметься начинка, і все просто чудово.

Мої запитання: Як я можу визначити 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

Це виглядає здорово , але я не зовсім розуміють код, особ в «екземпляр», save_m2m і old_save_m2m :)
єт

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. Але якщо до a 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

form.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, який визначає модель-посередник. Використовуйте вимоги. Натомість менеджер AssetRequirement.
Eloy Roldán Paredes

7

Чесно кажучи, я б вклав у модель відношення багато-до-багатьох Pizza. Я думаю, це ближче до реальності. Уявіть людину, яка замовляє кілька піц. Він би не сказав: "Я хотів би сиру на піцу один і два і помідори на піцу один і три", але, мабуть, "Одна піца з сиром, одна піца з сиром і помідорами, ...".

Звичайно, можна отримати форму, яка працює по-вашому, але я хотів би:

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

5
Моделі Pizza / Topping - це лише маскування для моїх справжніх моделей. Мета цього запитання полягає в тому, що я хочу, щоб Pizza ModelForm дозволяла мені вибирати начинку, а я хочу, щоб Topping ModelForm дозволяла мені вибирати піцу.
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)

Я припускаю, що це набирає чинності лише на адміністративних сторінках / у формах. Як би ви створили подібне для анонімних / запрошених користувачів та / або зареєстрованих користувачів, щоб мати можливість опублікувати, наприклад, перевагу піци?
користувач1271930

2

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

new_pizza.topping_set.add(new_topping)

2

У нас була подібна проблема в нашому додатку, який використовував адміністратора django. Між користувачами та групами існує багато-багато стосунків, і неможливо легко додати користувачів до групи. Я створив патч для 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

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 на начинку, тому що це дозволяє користувачеві вибрати, які начинки піти на піцу, яку вони хотіли. Просте, але потужне рішення.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.